Skip to main content
Glama

MCP Atlassian

by uchinx
MIT License
  • Apple
  • Linux
formatting.py10.8 kB
"""Module for Jira content formatting utilities.""" import html import logging import re from typing import Any from ..preprocessing.jira import JiraPreprocessor from .client import JiraClient from .protocols import ( EpicOperationsProto, FieldsOperationsProto, IssueOperationsProto, UsersOperationsProto, ) logger = logging.getLogger("mcp-jira") class FormattingMixin( JiraClient, EpicOperationsProto, FieldsOperationsProto, IssueOperationsProto, UsersOperationsProto, ): """Mixin for Jira content formatting operations. This mixin provides utilities for converting between different formats, formatting issue content for display, parsing dates, and sanitizing content. """ def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the FormattingMixin. Args: *args: Positional arguments for the JiraClient **kwargs: Keyword arguments for the JiraClient """ super().__init__(*args, **kwargs) # Use the JiraPreprocessor with the base URL from the client base_url = "" if hasattr(self, "config") and hasattr(self.config, "url"): base_url = self.config.url self.preprocessor = JiraPreprocessor(base_url=base_url) def markdown_to_jira(self, markdown_text: str) -> str: """ Convert Markdown syntax to Jira markup syntax. This method uses the TextPreprocessor implementation for consistent conversion between Markdown and Jira markup. Args: markdown_text: Text in Markdown format Returns: Text in Jira markup format """ if not markdown_text: return "" try: # Use the existing preprocessor return self.preprocessor.markdown_to_jira(markdown_text) except Exception as e: logger.warning(f"Error converting markdown to Jira format: {str(e)}") # Return the original text if conversion fails return markdown_text def format_issue_content( self, issue_key: str, issue: dict[str, Any], description: str, comments: list[dict[str, Any]], created_date: str, epic_info: dict[str, str | None], ) -> str: """ Format the issue content for display. Args: issue_key: The issue key issue: The issue data from Jira description: Processed description text comments: List of comment dictionaries created_date: Formatted created date epic_info: Dictionary with epic_key and epic_name Returns: Formatted content string """ # Basic issue information content = f"""Issue: {issue_key} Title: {issue["fields"].get("summary", "")} Type: {issue["fields"]["issuetype"]["name"]} Status: {issue["fields"]["status"]["name"]} Created: {created_date} """ # Add Epic information if available if epic_info.get("epic_key"): content += f"Epic: {epic_info['epic_key']}" if epic_info.get("epic_name"): content += f" - {epic_info['epic_name']}" content += "\n" content += f""" Description: {description} """ # Add comments if present if comments: content += "\nComments:\n" + "\n".join( [f"{c['created']} - {c['author']}: {c['body']}" for c in comments] ) return content def create_issue_metadata( self, issue_key: str, issue: dict[str, Any], comments: list[dict[str, Any]], created_date: str, epic_info: dict[str, str | None], ) -> dict[str, Any]: """ Create metadata for the issue document. Args: issue_key: The issue key issue: The issue data from Jira comments: List of comment dictionaries created_date: Formatted created date epic_info: Dictionary with epic_key and epic_name Returns: Metadata dictionary """ # Extract fields fields = issue.get("fields", {}) # Basic metadata metadata = { "key": issue_key, "summary": fields.get("summary", ""), "type": fields.get("issuetype", {}).get("name", ""), "status": fields.get("status", {}).get("name", ""), "created": created_date, "source": "jira", } # Add assignee if present if fields.get("assignee"): metadata["assignee"] = fields["assignee"].get( "displayName", fields["assignee"].get("name", "") ) # Add reporter if present if fields.get("reporter"): metadata["reporter"] = fields["reporter"].get( "displayName", fields["reporter"].get("name", "") ) # Add priority if present if fields.get("priority"): metadata["priority"] = fields["priority"].get("name", "") # Add Epic information to metadata if available if epic_info.get("epic_key"): metadata["epic_key"] = epic_info["epic_key"] if epic_info.get("epic_name"): metadata["epic_name"] = epic_info["epic_name"] # Add project information if fields.get("project"): metadata["project"] = fields["project"].get("key", "") metadata["project_name"] = fields["project"].get("name", "") # Add comment count metadata["comment_count"] = len(comments) return metadata def extract_epic_information( self, issue: dict[str, Any] ) -> dict[str, None] | dict[str, str]: """ Extract epic information from issue data. Args: issue: Issue data dictionary Returns: Dictionary containing epic_key and epic_name (or None if not found) """ epic_info = {"epic_key": None, "epic_name": None} # Check if the issue has fields if "fields" not in issue: return epic_info fields = issue["fields"] # Try to get the epic link from issue # (requires the correct field ID which varies across instances) # Use the field discovery mechanism if available try: field_ids = self.get_field_ids_to_epic() # Get the epic link field ID epic_link_field = field_ids.get("Epic Link") if ( epic_link_field and epic_link_field in fields and fields[epic_link_field] ): epic_info["epic_key"] = fields[epic_link_field] # If the issue is linked to an epic, try to get the epic name if epic_info["epic_key"] and hasattr(self, "get_issue"): try: epic_issue = self.get_issue(epic_info["epic_key"]) epic_fields = epic_issue.get("fields", {}) # Get the epic name field ID epic_name_field = field_ids.get("Epic Name") if epic_name_field and epic_name_field in epic_fields: epic_info["epic_name"] = epic_fields[epic_name_field] except Exception as e: logger.warning(f"Error getting epic details: {str(e)}") except Exception as e: logger.warning(f"Error extracting epic information: {str(e)}") return epic_info def sanitize_html(self, html_content: str) -> str: """ Sanitize HTML content by removing HTML tags. Args: html_content: HTML content to sanitize Returns: Plaintext content with HTML tags removed """ if not html_content: return "" try: # Remove HTML tags plain_text = re.sub(r"<[^>]+>", "", html_content) # Decode HTML entities plain_text = html.unescape(plain_text) # Normalize whitespace plain_text = re.sub(r"\s+", " ", plain_text).strip() return plain_text except Exception as e: logger.warning(f"Error sanitizing HTML: {str(e)}") return html_content def sanitize_transition_fields(self, fields: dict[str, Any]) -> dict[str, Any]: """ Sanitize fields to ensure they're valid for the Jira API. This is used for transition data to properly format field values. Args: fields: Dictionary of fields to sanitize Returns: Dictionary of sanitized fields """ sanitized_fields = {} for key, value in fields.items(): # Skip empty values if value is None: continue # Handle assignee field specially if key in ["assignee", "reporter"]: # If the value is already a dictionary, use it as is if isinstance(value, dict) and "accountId" in value: sanitized_fields[key] = value else: # Otherwise, look up the account ID if not isinstance(value, str): logger.warning(f"Invalid assignee value: {value}") continue try: account_id = self._get_account_id(value) if account_id: sanitized_fields[key] = {"accountId": account_id} except Exception as e: logger.warning( f"Error getting account ID for {value}: {str(e)}" ) # All other fields pass through as is else: sanitized_fields[key] = value return sanitized_fields def add_comment_to_transition_data( self, transition_data: dict[str, Any], comment: str | None ) -> dict[str, Any]: """ Add a comment to transition data. Args: transition_data: Transition data dictionary comment: Comment text (in Markdown format) or None Returns: Updated transition data """ if not comment: return transition_data # Convert markdown to Jira format jira_formatted_comment = self.markdown_to_jira(comment) # Add the comment to the transition data transition_data["update"] = { "comment": [{"add": {"body": jira_formatted_comment}}] } return transition_data

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/uchinx/mcp-atlassian'

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