We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/sooperset/mcp-atlassian'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""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 = ""
disable_translation = False
if hasattr(self, "config") and hasattr(self.config, "url"):
base_url = self.config.url
if hasattr(self, "config") and hasattr(
self.config, "disable_jira_markup_translation"
):
disable_translation = self.config.disable_jira_markup_translation
self.preprocessor = JiraPreprocessor(
base_url=base_url, disable_translation=disable_translation
)
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