Skip to main content
Glama

Jira MCP Server

server.py65.8 kB
import asyncio import json import logging import os import sys from enum import Enum from pathlib import Path from typing import Any, Dict, List, Optional, Sequence, Union # --- Setup a dedicated file logger --- log_file_path = Path(__file__).parent / "jira_mcp_debug.log" logger = logging.getLogger("JiraMCPLogger") logger.setLevel(logging.DEBUG) # Capture all levels of logs # Create a file handler to write logs to a file # Use 'w' to overwrite the file on each run, ensuring a clean log handler = logging.FileHandler(log_file_path, mode="w") handler.setLevel(logging.DEBUG) # Create a formatter to make the logs readable formatter = logging.Formatter( "%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s" ) handler.setFormatter(formatter) # Add the handler to the logger if not logger.handlers: logger.addHandler(handler) logger.info("Logger initialized. All subsequent logs will go to jira_mcp_debug.log") # --- End of logger setup --- try: from jira import JIRA except ImportError: from .jira import JIRA from pydantic import BaseModel from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import EmbeddedResource, ImageContent, TextContent, Tool from .jira_v3_api import JiraV3APIClient try: from dotenv import load_dotenv # Try to load from .env file if it exists env_path = Path(__file__).parent.parent.parent.parent / ".env" if env_path.exists(): load_dotenv(dotenv_path=env_path) except ImportError: # dotenv is optional pass class JiraTools(str, Enum): GET_PROJECTS = "get_jira_projects" GET_ISSUE = "get_jira_issue" SEARCH_ISSUES = "search_jira_issues" CREATE_ISSUE = "create_jira_issue" CREATE_ISSUES = "create_jira_issues" ADD_COMMENT = "add_jira_comment" GET_TRANSITIONS = "get_jira_transitions" TRANSITION_ISSUE = "transition_jira_issue" CREATE_PROJECT = "create_jira_project" GET_PROJECT_ISSUE_TYPES = "get_jira_project_issue_types" class JiraIssueField(BaseModel): name: str value: str class JiraIssueResult(BaseModel): key: str summary: str description: Optional[str] = None status: Optional[str] = None assignee: Optional[str] = None reporter: Optional[str] = None created: Optional[str] = None updated: Optional[str] = None fields: Optional[Dict[str, Any]] = None comments: Optional[List[Dict[str, Any]]] = None watchers: Optional[Dict[str, Any]] = None attachments: Optional[List[Dict[str, Any]]] = None subtasks: Optional[List[Dict[str, Any]]] = None project: Optional[Dict[str, Any]] = None issue_links: Optional[List[Dict[str, Any]]] = None worklog: Optional[List[Dict[str, Any]]] = None timetracking: Optional[Dict[str, Any]] = None class JiraProjectResult(BaseModel): key: str name: str id: str lead: Optional[str] = None class JiraTransitionResult(BaseModel): id: str name: str class JiraServer: def __init__( self, server_url: str = None, auth_method: str = None, username: str = None, password: str = None, token: str = None, ): self.server_url = server_url self.auth_method = auth_method self.username = username self.password = password self.token = token self._v3_api_client = JiraV3APIClient( server_url=self.server_url, username=self.username, token=self.token, password=password, ) self.client = None def connect(self): """Connect to Jira server using provided authentication details""" if not self.server_url: print("Error: Jira server URL not provided") return False error_messages = [] # Try multiple auth methods if possible try: # First, try the specified auth method if self.auth_method == "basic_auth": # Basic auth - either username/password or username/token if self.username and self.password: try: print(f"Trying basic_auth with username and password") self.client = JIRA( server=self.server_url, basic_auth=(self.username, self.password), ) print("Connection successful with username/password") return True except Exception as e: error_msg = f"Failed basic_auth with username/password: {type(e).__name__}: {str(e)}" print(error_msg) error_messages.append(error_msg) if self.username and self.token: try: print(f"Trying basic_auth with username and API token") self.client = JIRA( server=self.server_url, basic_auth=(self.username, self.token), ) print("Connection successful with username/token") return True except Exception as e: error_msg = f"Failed basic_auth with username/token: {type(e).__name__}: {str(e)}" print(error_msg) error_messages.append(error_msg) print("Error: Username and password/token required for basic auth") error_messages.append( "Username and password/token required for basic auth" ) elif self.auth_method == "token_auth": # Token auth - just need the token if self.token: try: print(f"Trying token_auth with token") self.client = JIRA( server=self.server_url, token_auth=self.token ) print("Connection successful with token_auth") return True except Exception as e: error_msg = f"Failed token_auth: {type(e).__name__}: {str(e)}" print(error_msg) error_messages.append(error_msg) else: print("Error: Token required for token auth") error_messages.append("Token required for token auth") # If we're here and have a token, try using it with basic_auth for Jira Cloud # (even if auth_method wasn't basic_auth) if self.token and self.username and not self.client: try: print(f"Trying fallback to basic_auth with username and token") self.client = JIRA( server=self.server_url, basic_auth=(self.username, self.token) ) print("Connection successful with fallback basic_auth") return True except Exception as e: error_msg = ( f"Failed fallback to basic_auth: {type(e).__name__}: {str(e)}" ) print(error_msg) error_messages.append(error_msg) # If we're here and have a token, try using token_auth as a fallback # (even if auth_method wasn't token_auth) if self.token and not self.client: try: print(f"Trying fallback to token_auth") self.client = JIRA(server=self.server_url, token_auth=self.token) print("Connection successful with fallback token_auth") return True except Exception as e: error_msg = ( f"Failed fallback to token_auth: {type(e).__name__}: {str(e)}" ) print(error_msg) error_messages.append(error_msg) # Last resort: try anonymous access try: print(f"Trying anonymous access as last resort") self.client = JIRA(server=self.server_url) print("Connection successful with anonymous access") return True except Exception as e: error_msg = f"Failed anonymous access: {type(e).__name__}: {str(e)}" print(error_msg) error_messages.append(error_msg) # If we got here, all connection attempts failed print(f"All connection attempts failed: {', '.join(error_messages)}") return False except Exception as e: error_msg = f"Unexpected error in connect(): {type(e).__name__}: {str(e)}" print(error_msg) error_messages.append(error_msg) return False def _get_v3_api_client(self) -> JiraV3APIClient: """Get or create a v3 API client instance""" if not self._v3_api_client: self._v3_api_client = JiraV3APIClient( server_url=self.server_url, username=self.username, password=self.password, token=self.token, ) return self._v3_api_client async def get_jira_projects(self) -> List[JiraProjectResult]: """Get all accessible Jira projects using v3 REST API""" logger.info("Starting get_jira_projects...") all_projects_data = [] start_at = 0 max_results = 50 page_count = 0 while True: page_count += 1 logger.info( f"Pagination loop, page {page_count}: startAt={start_at}, maxResults={max_results}" ) try: response = await self._v3_api_client.get_projects( start_at=start_at, max_results=max_results ) projects = response.get("values", []) if not projects: logger.info("No more projects returned. Breaking pagination loop.") break all_projects_data.extend(projects) if response.get("isLast", False): logger.info("'isLast' is True. Breaking pagination loop.") break start_at += len(projects) # Yield control to the event loop to prevent deadlocks in the MCP framework. await asyncio.sleep(0) except Exception as e: logger.error( "Error inside get_jira_projects pagination loop", exc_info=True ) raise logger.info( f"Finished get_jira_projects. Total projects found: {len(all_projects_data)}" ) results = [] for p in all_projects_data: results.append( JiraProjectResult( key=p.get("key"), name=p.get("name"), id=str(p.get("id")), lead=(p.get("lead") or {}).get("displayName"), ) ) logger.info(f"Added project {p.get('key')} to results") logger.info(f"Returning {len(results)} projects") sys.stdout.flush() # Flush stdout to ensure it's sent to MCP, otherwise hang occurs return results def get_jira_issue(self, issue_key: str) -> JiraIssueResult: """Get details for a specific issue by key""" if not self.client: if not self.connect(): # Connection failed - provide clear error message raise ValueError( f"Failed to connect to Jira server at {self.server_url}. Check your authentication credentials." ) try: issue = self.client.issue(issue_key) # Extract comments if available comments = [] if hasattr(issue.fields, "comment") and hasattr( issue.fields.comment, "comments" ): for comment in issue.fields.comment.comments: comments.append( { "author": ( getattr( comment.author, "displayName", str(comment.author) ) if hasattr(comment, "author") else "Unknown" ), "body": comment.body, "created": comment.created, } ) # Create a fields dictionary with custom fields fields = {} for field_name in dir(issue.fields): if not field_name.startswith("_") and field_name not in [ "comment", "attachment", "summary", "description", "status", "assignee", "reporter", "created", "updated", ]: value = getattr(issue.fields, field_name) if value is not None: # Handle special field types if hasattr(value, "name"): fields[field_name] = value.name elif hasattr(value, "value"): fields[field_name] = value.value elif isinstance(value, list): if len(value) > 0: if hasattr(value[0], "name"): fields[field_name] = [item.name for item in value] else: fields[field_name] = value else: fields[field_name] = str(value) return JiraIssueResult( key=issue.key, summary=issue.fields.summary, description=issue.fields.description, status=( issue.fields.status.name if hasattr(issue.fields, "status") else None ), assignee=( issue.fields.assignee.displayName if hasattr(issue.fields, "assignee") and issue.fields.assignee else None ), reporter=( issue.fields.reporter.displayName if hasattr(issue.fields, "reporter") and issue.fields.reporter else None ), created=( issue.fields.created if hasattr(issue.fields, "created") else None ), updated=( issue.fields.updated if hasattr(issue.fields, "updated") else None ), fields=fields, comments=comments, ) except Exception as e: print(f"Failed to get issue {issue_key}: {type(e).__name__}: {str(e)}") raise ValueError( f"Failed to get issue {issue_key}: {type(e).__name__}: {str(e)}" ) async def search_jira_issues( self, jql: str, max_results: int = 10 ) -> List[JiraIssueResult]: """Search for issues using JQL via v3 REST API with pagination support""" logger.info("Starting search_jira_issues...") try: # Use v3 API client v3_client = self._get_v3_api_client() # Collect all issues from all pages all_issues = [] start_at = 0 page_size = min(max_results, 100) # Jira typically limits to 100 per page while True: logger.debug(f"Fetching page starting at {start_at} with page size {page_size}") response_data = await v3_client.search_issues( jql=jql, start_at=start_at, max_results=page_size ) # Extract issues from current page page_issues = response_data.get("issues", []) all_issues.extend(page_issues) logger.debug(f"Retrieved {len(page_issues)} issues from current page. Total so far: {len(all_issues)}") # Check if we've reached the user's max_results limit if len(all_issues) >= max_results: # Trim to exact max_results if we exceeded it all_issues = all_issues[:max_results] logger.debug(f"Reached max_results limit of {max_results}, stopping pagination") break # Check if this is the last page according to API is_last = response_data.get("isLast", True) if is_last: logger.debug("API indicates this is the last page, stopping pagination") break # If we have more pages, prepare for next iteration start_at = len(all_issues) # Use actual number of issues retrieved so far # Adjust page size for next request to not exceed max_results remaining_needed = max_results - len(all_issues) page_size = min(remaining_needed, 100) # Return raw issues list for full JSON data logger.info(f"Returning raw issues ({len(all_issues)}) for JQL: {jql}") return all_issues except Exception as e: error_msg = f"Failed to search issues: {type(e).__name__}: {str(e)}" logger.error(error_msg, exc_info=True) print(error_msg) raise ValueError(error_msg) async def create_jira_issue( self, project: str, summary: str, description: str, issue_type: str, fields: Optional[Dict[str, Any]] = None, ) -> JiraIssueResult: """Create a new Jira issue using v3 REST API Args: project: Project key (e.g., 'PROJ') summary: Issue summary/title description: Issue description issue_type: Issue type - common values include 'Bug', 'Task', 'Story', 'Epic', 'New Feature', 'Improvement' Note: Available issue types vary by Jira instance and project fields: Optional additional fields dictionary Returns: JiraIssueResult object with the created issue details Example: # Create a bug await create_jira_issue( project='PROJ', summary='Login button not working', description='The login button on the homepage is not responding to clicks', issue_type='Bug' ) # Create a task with custom fields await create_jira_issue( project='PROJ', summary='Update documentation', description='Update API documentation with new endpoints', issue_type='Task', fields={ 'assignee': 'jsmith', 'labels': ['documentation', 'api'], 'priority': {'name': 'High'} } ) """ logger.info("Starting create_jira_issue...") try: # Create a properly formatted issue dictionary issue_dict = {} # Process required fields first # Project field - required if isinstance(project, str): issue_dict["project"] = {"key": project} else: issue_dict["project"] = project # Summary - required issue_dict["summary"] = summary # Description if description: issue_dict["description"] = description # Issue type - required, with validation for common issue types logger.info( f"Processing issue_type: '{issue_type}' (type: {type(issue_type)})" ) common_types = [ "bug", "task", "story", "epic", "improvement", "newfeature", "new feature", ] if isinstance(issue_type, str): # Check for common issue type variants and fix case-sensitivity issues issue_type_lower = issue_type.lower() if issue_type_lower in common_types: # Convert first letter to uppercase for standard Jira types issue_type_proper = issue_type_lower.capitalize() if ( issue_type_lower == "new feature" or issue_type_lower == "newfeature" ): issue_type_proper = "New Feature" logger.info( f"Note: Converting issue type from '{issue_type}' to '{issue_type_proper}'" ) issue_dict["issuetype"] = {"name": issue_type_proper} else: # Use the type as provided - some Jira instances have custom types issue_dict["issuetype"] = {"name": issue_type} else: issue_dict["issuetype"] = issue_type # Add any additional fields with proper type handling if fields: for key, value in fields.items(): # Skip fields we've already processed if key in [ "project", "summary", "description", "issuetype", "issue_type", ]: continue # Handle special fields that require specific formats if key == "assignees" or key == "assignee": # Convert string to array for assignees or proper format for assignee if isinstance(value, str): if key == "assignees": issue_dict[key] = [value] if value else [] else: # assignee issue_dict[key] = {"name": value} if value else None elif isinstance(value, list) and key == "assignee" and value: # If assignee is a list but should be a dict with name issue_dict[key] = {"name": value[0]} else: issue_dict[key] = value elif key == "labels": # Convert string to array for labels if isinstance(value, str): issue_dict[key] = [value] if value else [] else: issue_dict[key] = value elif key == "milestone": # Convert string to number for milestone if isinstance(value, str) and value.isdigit(): issue_dict[key] = int(value) else: issue_dict[key] = value else: issue_dict[key] = value # Use v3 API client v3_client = self._get_v3_api_client() response_data = await v3_client.create_issue(fields=issue_dict) # Extract issue details from v3 API response issue_key = response_data.get("key") issue_id = response_data.get("id") logger.info(f"Successfully created issue {issue_key} (ID: {issue_id})") # Return JiraIssueResult with the created issue details # For v3 API, we return what we have from the create response return JiraIssueResult( key=issue_key, summary=summary, # Use the summary we provided description=description, # Use the description we provided status="Open", # Default status for new issues ) except Exception as e: error_msg = f"Failed to create issue: {type(e).__name__}: {str(e)}" logger.error(error_msg, exc_info=True) # Enhanced error handling for issue type errors if "issuetype" in str(e).lower() or "issue type" in str(e).lower(): logger.info( "Issue type error detected, trying to provide helpful suggestions..." ) try: project_key = ( project if isinstance(project, str) else project.get("key") ) if project_key: issue_types = await self.get_jira_project_issue_types( project_key ) type_names = [t.get("name") for t in issue_types] logger.info( f"Available issue types for project {project_key}: {', '.join(type_names)}" ) # Try to find the closest match attempted_type = issue_type closest = None attempted_lower = attempted_type.lower() for t in type_names: if ( attempted_lower in t.lower() or t.lower() in attempted_lower ): closest = t break if closest: logger.info( f"The closest match to '{attempted_type}' is '{closest}'" ) error_msg += f" Available types: {', '.join(type_names)}. Closest match: '{closest}'" else: error_msg += f" Available types: {', '.join(type_names)}" except Exception as fetch_error: logger.error(f"Could not fetch issue types: {str(fetch_error)}") raise ValueError(error_msg) # Re-raise the exception with more details if "issuetype" in error_message.lower(): raise ValueError( f"Invalid issue type '{issue_dict.get('issuetype', {}).get('name', 'Unknown')}'. " + "Use get_jira_project_issue_types(project_key) to get valid types." ) raise return JiraIssueResult( key=new_issue.key, summary=new_issue.fields.summary, description=new_issue.fields.description, status=( new_issue.fields.status.name if hasattr(new_issue.fields, "status") else None ), ) except Exception as e: print(f"Failed to create issue: {type(e).__name__}: {str(e)}") raise ValueError(f"Failed to create issue: {type(e).__name__}: {str(e)}") async def create_jira_issues( self, field_list: List[Dict[str, Any]], prefetch: bool = True ) -> List[Dict[str, Any]]: """Bulk create new Jira issues using v3 REST API. Parameters: field_list (List[Dict[str, Any]]): a list of dicts each containing field names and the values to use. Each dict is an individual issue to create. prefetch (bool): True reloads the created issue Resource so all of its data is present in the value returned (Default: True) Returns: List[Dict[str, Any]]: List of created issues with their details Issue Types: Common issue types include: 'Bug', 'Task', 'Story', 'Epic', 'New Feature', 'Improvement' Note: Available issue types vary by Jira instance and project Example: # Create multiple issues in bulk await create_jira_issues([ { 'project': 'PROJ', 'summary': 'Implement user authentication', 'description': 'Add login and registration functionality', 'issue_type': 'Story' # Note: case-sensitive, match to your Jira instance types }, { 'project': 'PROJ', 'summary': 'Fix navigation bar display on mobile', 'description': 'Navigation bar is not displaying correctly on mobile devices', 'issue_type': 'Bug', 'priority': {'name': 'High'}, 'labels': ['mobile', 'ui'] } ]) """ logger.info("Starting create_jira_issues...") try: # Process each field dict to ensure proper formatting for v3 API processed_field_list = [] for fields in field_list: # Create a properly formatted issue dictionary issue_dict = {} # Process required fields first to ensure they exist # Project field - required if "project" not in fields: raise ValueError("Each issue must have a 'project' field") project_value = fields["project"] if isinstance(project_value, str): issue_dict["project"] = {"key": project_value} else: issue_dict["project"] = project_value # Summary field - required if "summary" not in fields: raise ValueError("Each issue must have a 'summary' field") issue_dict["summary"] = fields["summary"] # Description field - convert to ADF format for v3 API if it's a simple string if "description" in fields: description = fields["description"] if isinstance(description, str): # Convert simple string to Atlassian Document Format issue_dict["description"] = { "type": "doc", "version": 1, "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": description } ] } ] } else: # Assume it's already in ADF format issue_dict["description"] = description # Issue type field - required, handle both 'issuetype' and 'issue_type' issue_type = None if "issuetype" in fields: issue_type = fields["issuetype"] elif "issue_type" in fields: issue_type = fields["issue_type"] else: raise ValueError( "Each issue must have an 'issuetype' or 'issue_type' field" ) # Check for common issue type variants and fix case-sensitivity issues logger.debug( f"Processing bulk issue_type: '{issue_type}' (type: {type(issue_type)})" ) common_types = [ "bug", "task", "story", "epic", "improvement", "newfeature", "new feature", ] if isinstance(issue_type, str): issue_type_lower = issue_type.lower() if issue_type_lower in common_types: # Convert first letter to uppercase for standard Jira types issue_type_proper = issue_type_lower.capitalize() if ( issue_type_lower == "new feature" or issue_type_lower == "newfeature" ): issue_type_proper = "New Feature" logger.debug( f"Converting issue type from '{issue_type}' to '{issue_type_proper}'" ) issue_dict["issuetype"] = {"name": issue_type_proper} else: # Use the type as provided - some Jira instances have custom types issue_dict["issuetype"] = {"name": issue_type} else: issue_dict["issuetype"] = issue_type # Process other fields for key, value in fields.items(): if key in [ "project", "summary", "description", "issuetype", "issue_type", ]: # Skip fields we've already processed continue # Handle special fields that require specific formats if key == "assignees" or key == "assignee": # Convert string to array for assignees or proper format for assignee if isinstance(value, str): if key == "assignees": issue_dict[key] = [value] if value else [] else: # assignee issue_dict[key] = {"name": value} if value else None elif isinstance(value, list) and key == "assignee" and value: # If assignee is a list but should be a dict with name issue_dict[key] = {"name": value[0]} else: issue_dict[key] = value elif key == "labels": # Convert string to array for labels if isinstance(value, str): issue_dict[key] = [value] if value else [] else: issue_dict[key] = value elif key == "milestone": # Convert string to number for milestone if isinstance(value, str) and value.isdigit(): issue_dict[key] = int(value) else: issue_dict[key] = value else: issue_dict[key] = value # Add to the field list in v3 API format processed_field_list.append({"fields": issue_dict}) logger.debug(f"Processed field list: {json.dumps(processed_field_list, indent=2)}") # Use v3 API client v3_client = self._get_v3_api_client() # Call the bulk create API response_data = await v3_client.bulk_create_issues(processed_field_list) # Process the results to maintain compatibility with existing interface processed_results = [] # Handle successful issues if "issues" in response_data: for issue in response_data["issues"]: processed_results.append({ "key": issue.get("key"), "id": issue.get("id"), "self": issue.get("self"), "success": True, }) # Handle errors if "errors" in response_data: for error in response_data["errors"]: processed_results.append({ "error": error, "success": False, }) logger.info(f"Successfully processed {len(processed_results)} issue creations") return processed_results except Exception as e: error_msg = f"Failed to create issues in bulk: {type(e).__name__}: {str(e)}" logger.error(error_msg, exc_info=True) print(error_msg) raise ValueError(error_msg) async def add_jira_comment(self, issue_key: str, comment: str) -> Dict[str, Any]: """Add a comment to an issue using v3 REST API""" logger.info("Starting add_jira_comment...") try: # Use v3 API client v3_client = self._get_v3_api_client() comment_result = await v3_client.add_comment( issue_id_or_key=issue_key, comment=comment, ) # Extract useful information from the v3 API response response_data = { "id": comment_result.get("id"), "body": comment_result.get("body", {}), "created": comment_result.get("created"), "updated": comment_result.get("updated"), } # Extract author information if available if "author" in comment_result: author = comment_result["author"] response_data["author"] = author.get("displayName", "Unknown") else: response_data["author"] = "Unknown" logger.info(f"Successfully added comment to issue {issue_key}") return response_data except Exception as e: error_msg = ( f"Failed to add comment to {issue_key}: {type(e).__name__}: {str(e)}" ) logger.error(error_msg, exc_info=True) print(error_msg) raise ValueError(error_msg) async def get_jira_transitions(self, issue_key: str) -> List[JiraTransitionResult]: """Get available transitions for an issue using v3 REST API""" logger.info("Starting get_jira_transitions...") try: # Use v3 API client v3_client = self._get_v3_api_client() response_data = await v3_client.get_transitions(issue_id_or_key=issue_key) # Extract transitions from response transitions = response_data.get("transitions", []) # Convert to JiraTransitionResult objects maintaining compatibility results = [ JiraTransitionResult(id=transition["id"], name=transition["name"]) for transition in transitions ] logger.info(f"Found {len(results)} transitions for issue {issue_key}") return results except Exception as e: error_msg = f"Failed to get transitions for {issue_key}: {type(e).__name__}: {str(e)}" logger.error(error_msg, exc_info=True) print(error_msg) raise ValueError(error_msg) async def transition_jira_issue( self, issue_key: str, transition_id: str, comment: Optional[str] = None, fields: Optional[Dict[str, Any]] = None, ) -> bool: """Transition an issue to a new state using v3 REST API""" logger.info("Starting transition_jira_issue...") try: # Use v3 API client v3_client = self._get_v3_api_client() await v3_client.transition_issue( issue_id_or_key=issue_key, transition_id=transition_id, fields=fields, comment=comment, ) logger.info( f"Successfully transitioned issue {issue_key} to transition {transition_id}" ) return True except Exception as e: error_msg = ( f"Failed to transition {issue_key}: {type(e).__name__}: {str(e)}" ) logger.error(error_msg, exc_info=True) print(error_msg) raise ValueError(error_msg) async def get_jira_project_issue_types( self, project_key: str ) -> List[Dict[str, Any]]: """Get all available issue types for a specific project using v3 REST API Args: project_key: The project key (e.g., 'PROJ') - kept for backward compatibility, but the new API returns all issue types for the user Returns: List of issue type dictionaries with name, id, and description Example: get_jira_project_issue_types('PROJ') # Returns all issue types accessible to user """ logger.info("Starting get_jira_project_issue_types...") try: # Use v3 API client to get all issue types v3_client = self._get_v3_api_client() response_data = await v3_client.get_issue_types() # The new API returns the issue types directly as a list, not wrapped in an object issue_types_data = ( response_data if isinstance(response_data, list) else response_data.get("issueTypes", []) ) # Convert to the expected format maintaining compatibility issue_types = [] for issuetype in issue_types_data: issue_types.append( { "id": issuetype.get("id"), "name": issuetype.get("name"), "description": issuetype.get("description"), } ) logger.info( f"Found {len(issue_types)} issue types (project_key: {project_key})" ) return issue_types except Exception as e: error_msg = f"Failed to get issue types: {type(e).__name__}: {str(e)}" logger.error(error_msg, exc_info=True) print(error_msg) raise ValueError(error_msg) async def create_jira_project( self, key: str, name: Optional[str] = None, assignee: Optional[str] = None, ptype: str = "software", template_name: Optional[str] = None, avatarId: Optional[int] = None, issueSecurityScheme: Optional[int] = None, permissionScheme: Optional[int] = None, projectCategory: Optional[int] = None, notificationScheme: Optional[int] = None, categoryId: Optional[int] = None, url: str = "", ) -> JiraProjectResult: """Create a project using Jira's v3 REST API Args: key: Project key (required) - must match Jira project key requirements name: Project name (defaults to key if not provided) assignee: Lead account ID or username ptype: Project type key ('software', 'business', 'service_desk') template_name: Project template key for creating from templates avatarId: ID of the avatar to use for the project issueSecurityScheme: ID of the issue security scheme permissionScheme: ID of the permission scheme projectCategory: ID of the project category notificationScheme: ID of the notification scheme categoryId: Same as projectCategory (alternative parameter) url: URL for project information/documentation Returns: JiraProjectResult with the created project details Note: This method uses Jira's v3 REST API endpoint: POST /rest/api/3/project Example: # Create a basic software project create_jira_project( key='PROJ', name='My Project', ptype='software' ) # Create with template create_jira_project( key='BUSI', name='Business Project', ptype='business', template_name='com.atlassian.jira-core-project-templates:jira-core-simplified-task-tracking' ) """ if not key: raise ValueError("Project key is required") try: # Get the v3 API client v3_client = self._get_v3_api_client() # Create project using v3 API response_data = await v3_client.create_project( key=key, name=name, assignee=assignee, ptype=ptype, template_name=template_name, avatarId=avatarId, issueSecurityScheme=issueSecurityScheme, permissionScheme=permissionScheme, projectCategory=projectCategory, notificationScheme=notificationScheme, categoryId=categoryId, url=url, ) # Extract project details from response project_id = response_data.get("id", "0") project_key = response_data.get("key", key) # For lead information, we would need to make another API call # For now, return None for lead as it's optional in our result model lead = None return JiraProjectResult( key=project_key, name=name or key, id=str(project_id), lead=lead ) except Exception as e: error_msg = str(e) print(f"Error creating project with v3 API: {error_msg}") raise ValueError(f"Error creating project: {error_msg}") async def serve( server_url: Optional[str] = None, auth_method: Optional[str] = None, username: Optional[str] = None, password: Optional[str] = None, token: Optional[str] = None, ) -> None: server = Server("mcp-jira") jira_server = JiraServer( server_url=server_url, auth_method=auth_method, username=username, password=password, token=token, ) @server.list_tools() async def list_tools() -> list[Tool]: """List available Jira tools.""" return [ Tool( name=JiraTools.GET_PROJECTS.value, description="Get all accessible Jira projects", inputSchema={"type": "object", "properties": {}, "required": []}, ), Tool( name=JiraTools.GET_ISSUE.value, description="Get details for a specific Jira issue by key", inputSchema={ "type": "object", "properties": { "issue_key": { "type": "string", "description": "The issue key (e.g., PROJECT-123)", } }, "required": ["issue_key"], }, ), Tool( name=JiraTools.SEARCH_ISSUES.value, description="Search for Jira issues using JQL (Jira Query Language)", inputSchema={ "type": "object", "properties": { "jql": { "type": "string", "description": "JQL query string (e.g., 'project = MYPROJ AND status = \"In Progress\"')", }, "max_results": { "type": "integer", "description": "Maximum number of results to return (default: 10)", }, }, "required": ["jql"], }, ), Tool( name=JiraTools.CREATE_ISSUE.value, description="Create a new Jira issue. Common issue types include 'Bug', 'Task', 'Story', 'Epic' (capitalization handled automatically)", inputSchema={ "type": "object", "properties": { "project": { "type": "string", "description": "Project key (e.g., 'MYPROJ')", }, "summary": { "type": "string", "description": "Issue summary/title", }, "description": { "type": "string", "description": "Issue description", }, "issue_type": { "type": "string", "description": "Issue type (e.g., 'Bug', 'Task', 'Story', 'Epic', 'New Feature', 'Improvement'). IMPORTANT: Types are case-sensitive and vary by Jira instance.", }, "fields": { "type": "object", "description": "Additional fields for the issue (optional)", }, }, "required": ["project", "summary", "description", "issue_type"], }, ), Tool( name=JiraTools.CREATE_ISSUES.value, description="Bulk create new Jira issues. IMPORTANT: For 'issue_type', use the exact case-sensitive types in your Jira instance (common: 'Bug', 'Task', 'Story', 'Epic')", inputSchema={ "type": "object", "properties": { "field_list": { "type": "array", "description": "A list of field dictionaries, each representing an issue to create", "items": { "type": "object", "description": "Field dictionary for a single issue", }, }, "prefetch": { "type": "boolean", "description": "Whether to reload created issues (default: true)", }, }, "required": ["field_list"], }, ), Tool( name=JiraTools.ADD_COMMENT.value, description="Add a comment to a Jira issue", inputSchema={ "type": "object", "properties": { "issue_key": { "type": "string", "description": "The issue key (e.g., PROJECT-123)", }, "comment": { "type": "string", "description": "The comment text", }, }, "required": ["issue_key", "comment"], }, ), Tool( name=JiraTools.GET_TRANSITIONS.value, description="Get available workflow transitions for a Jira issue", inputSchema={ "type": "object", "properties": { "issue_key": { "type": "string", "description": "The issue key (e.g., PROJECT-123)", } }, "required": ["issue_key"], }, ), Tool( name=JiraTools.TRANSITION_ISSUE.value, description="Transition a Jira issue to a new status", inputSchema={ "type": "object", "properties": { "issue_key": { "type": "string", "description": "The issue key (e.g., PROJECT-123)", }, "transition_id": { "type": "string", "description": "ID of the transition to perform (get IDs using get_transitions)", }, "comment": { "type": "string", "description": "Comment to add during transition (optional)", }, "fields": { "type": "object", "description": "Additional fields to update during transition (optional)", }, }, "required": ["issue_key", "transition_id"], }, ), Tool( name=JiraTools.GET_PROJECT_ISSUE_TYPES.value, description="Get all available issue types for a specific Jira project", inputSchema={ "type": "object", "properties": { "project_key": { "type": "string", "description": "The project key (e.g., 'MYPROJ')", } }, "required": ["project_key"], }, ), Tool( name=JiraTools.CREATE_PROJECT.value, description="Create a new Jira project using v3 REST API", inputSchema={ "type": "object", "properties": { "key": { "type": "string", "description": "Mandatory. Must match Jira project key requirements, usually only 2-10 uppercase characters.", }, "name": { "type": "string", "description": "If not specified it will use the key value.", }, "assignee": { "type": "string", "description": "Lead account ID or username (mapped to leadAccountId in v3 API).", }, "ptype": { "type": "string", "description": "Project type key: 'software', 'business', or 'service_desk'. Defaults to 'software'.", }, "template_name": { "type": "string", "description": "Project template key for creating from templates (mapped to projectTemplateKey in v3 API).", }, "avatarId": { "type": ["integer", "string"], "description": "ID of the avatar to use for the project.", }, "issueSecurityScheme": { "type": ["integer", "string"], "description": "Determines the security scheme to use.", }, "permissionScheme": { "type": ["integer", "string"], "description": "Determines the permission scheme to use.", }, "projectCategory": { "type": ["integer", "string"], "description": "Determines the category the project belongs to.", }, "notificationScheme": { "type": ["integer", "string"], "description": "Determines the notification scheme to use. Default is None.", }, "categoryId": { "type": ["integer", "string"], "description": "Same as projectCategory. Can be used interchangeably.", }, "url": { "type": "string", "description": "A link to information about the project, such as documentation.", }, }, "required": ["key"], }, ), ] @server.call_tool() async def call_tool( name: str, arguments: dict ) -> Sequence[TextContent | ImageContent | EmbeddedResource]: """Handle tool calls for Jira operations.""" logger.info(f"call_tool invoked. Tool: '{name}', Arguments: {arguments}") try: result: Any match name: case JiraTools.GET_PROJECTS.value: logger.info("About to AWAIT jira_server.get_jira_projects...") result = await jira_server.get_jira_projects() logger.info( f"COMPLETED await jira_server.get_jira_projects. Result has {len(result)} items." ) case JiraTools.GET_ISSUE.value: logger.info("Calling synchronous tool get_jira_issue...") issue_key = arguments.get("issue_key") if not issue_key: raise ValueError("Missing required argument: issue_key") result = jira_server.get_jira_issue(issue_key) logger.info("Synchronous tool get_jira_issue completed.") case JiraTools.SEARCH_ISSUES.value: logger.info("Calling async tool search_jira_issues...") jql = arguments.get("jql") if not jql: raise ValueError("Missing required argument: jql") max_results = arguments.get("max_results", 10) result = await jira_server.search_jira_issues(jql, max_results) logger.info("Async tool search_jira_issues completed.") case JiraTools.CREATE_ISSUE.value: logger.info("About to AWAIT jira_server.create_jira_issue...") required_args = ["project", "summary", "description", "issue_type"] if not all(arg in arguments for arg in required_args): missing = [arg for arg in required_args if arg not in arguments] raise ValueError( f"Missing required arguments: {', '.join(missing)}" ) result = await jira_server.create_jira_issue( arguments["project"], arguments["summary"], arguments["description"], arguments["issue_type"], arguments.get("fields", {}), ) logger.info("COMPLETED await jira_server.create_jira_issue.") case JiraTools.CREATE_ISSUES.value: logger.info("Calling async tool create_jira_issues...") field_list = arguments.get("field_list") if not field_list: raise ValueError("Missing required argument: field_list") prefetch = arguments.get("prefetch", True) result = await jira_server.create_jira_issues(field_list, prefetch) logger.info("Async tool create_jira_issues completed.") case JiraTools.ADD_COMMENT.value: logger.info("About to AWAIT jira_server.add_jira_comment...") issue_key = arguments.get("issue_key") comment_text = arguments.get("comment") or arguments.get("body") if not issue_key or not comment_text: raise ValueError( "Missing required arguments: issue_key and comment (or body)" ) result = await jira_server.add_jira_comment(issue_key, comment_text) logger.info("COMPLETED await jira_server.add_jira_comment.") case JiraTools.GET_TRANSITIONS.value: logger.info("About to AWAIT jira_server.get_jira_transitions...") issue_key = arguments.get("issue_key") if not issue_key: raise ValueError("Missing required argument: issue_key") result = await jira_server.get_jira_transitions(issue_key) logger.info("COMPLETED await jira_server.get_jira_transitions.") case JiraTools.TRANSITION_ISSUE.value: logger.info("Calling async tool transition_jira_issue...") issue_key = arguments.get("issue_key") transition_id = arguments.get("transition_id") if not issue_key or not transition_id: raise ValueError( "Missing required arguments: issue_key and transition_id" ) comment = arguments.get("comment") fields = arguments.get("fields") result = await jira_server.transition_jira_issue( issue_key, transition_id, comment, fields ) logger.info("Async tool transition_jira_issue completed.") case JiraTools.GET_PROJECT_ISSUE_TYPES.value: logger.info( "Calling asynchronous tool get_jira_project_issue_types..." ) project_key = arguments.get("project_key") if not project_key: raise ValueError("Missing required argument: project_key") result = await jira_server.get_jira_project_issue_types(project_key) logger.info( "Asynchronous tool get_jira_project_issue_types completed." ) case JiraTools.CREATE_PROJECT.value: logger.info("About to AWAIT jira_server.create_jira_project...") key = arguments.get("key") if not key: raise ValueError("Missing required argument: key") # Type conversion logic from original code for int_key in [ "avatarId", "issueSecurityScheme", "permissionScheme", "projectCategory", "notificationScheme", "categoryId", ]: if ( int_key in arguments and isinstance(arguments[int_key], str) and arguments[int_key].isdigit() ): arguments[int_key] = int(arguments[int_key]) result = await jira_server.create_jira_project(**arguments) logger.info("COMPLETED await jira_server.create_jira_project.") case _: raise ValueError(f"Unknown tool: {name}") logger.debug("Serializing result to JSON...") # Handle serialization properly for different result types if isinstance(result, list): # If it's a list, check each item individually serialized_result = [] for item in result: if hasattr(item, "model_dump"): serialized_result.append(item.model_dump()) else: # It's already a dict or basic type serialized_result.append(item) else: # Single item result if hasattr(result, "model_dump"): serialized_result = result.model_dump() else: # It's already a dict or basic type serialized_result = result json_result = json.dumps(serialized_result, indent=2) return [TextContent(type="text", text=json_result)] except Exception as e: logger.critical( f"FATAL error in call_tool for tool '{name}'", exc_info=True ) return [ TextContent( type="text", text=json.dumps( { "error": f"Error in tool '{name}': {type(e).__name__}: {str(e)}" } ), ) ] options = server.create_initialization_options() async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, options)

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

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