Home Assistant MCP
by allenporter
- src
- mcp_atlassian
import logging
import os
from datetime import datetime
from typing import Any
from atlassian import Jira
from .config import JiraConfig
from .document_types import Document
from .preprocessing import TextPreprocessor
# Configure logging
logger = logging.getLogger("mcp-jira")
class JiraFetcher:
"""Handles fetching and parsing content from Jira."""
def __init__(self):
url = os.getenv("JIRA_URL")
username = os.getenv("JIRA_USERNAME")
token = os.getenv("JIRA_API_TOKEN")
if not all([url, username, token]):
raise ValueError("Missing required Jira environment variables")
self.config = JiraConfig(url=url, username=username, api_token=token)
self.jira = Jira(
url=self.config.url,
username=self.config.username,
password=self.config.api_token, # API token is used as password
cloud=True,
)
self.preprocessor = TextPreprocessor(self.config.url)
def _clean_text(self, text: str) -> str:
"""
Clean text content by:
1. Processing user mentions and links
2. Converting HTML/wiki markup to markdown
"""
if not text:
return ""
return self.preprocessor.clean_jira_text(text)
def create_issue(
self,
project_key: str,
summary: str,
issue_type: str,
description: str = "",
**kwargs: Any,
) -> Document:
"""
Create a new issue in Jira and return it as a Document.
Args:
project_key: The key of the project (e.g. 'PROJ')
summary: Summary of the issue
issue_type: Issue type (e.g. 'Task', 'Bug', 'Story')
description: Issue description
kwargs: Any other custom Jira fields
Returns:
Document representing the newly created issue
"""
fields = {
"project": {"key": project_key},
"summary": summary,
"issuetype": {"name": issue_type},
"description": description,
}
for key, value in kwargs.items():
fields[key] = value
try:
created = self.jira.issue_create(fields=fields)
issue_key = created.get("key")
if not issue_key:
raise ValueError(f"Failed to create issue in project {project_key}")
return self.get_issue(issue_key)
except Exception as e:
logger.error(f"Error creating issue in project {project_key}: {str(e)}")
raise
def update_issue(self, issue_key: str, fields: dict[str, Any] = None, **kwargs: Any) -> Document:
"""
Update an existing issue.
Args:
issue_key: The key of the issue (e.g. 'PROJ-123')
fields: Dictionary of fields to update
kwargs: Additional fields to update
Returns:
Document representing the updated issue
"""
fields = fields or {}
for k, v in kwargs.items():
fields[k] = v
try:
self.jira.issue_update(issue_key, fields=fields)
return self.get_issue(issue_key)
except Exception as e:
logger.error(f"Error updating issue {issue_key}: {str(e)}")
raise
def delete_issue(self, issue_key: str) -> bool:
"""
Delete an existing issue.
Args:
issue_key: The key of the issue (e.g. 'PROJ-123')
Returns:
True if delete succeeded, otherwise raise an exception
"""
try:
self.jira.delete_issue(issue_key)
return True
except Exception as e:
logger.error(f"Error deleting issue {issue_key}: {str(e)}")
raise
def _parse_date(self, date_str: str) -> str:
"""Parse date string to handle various ISO formats."""
if not date_str:
return ""
# Handle various timezone formats
if "+0000" in date_str:
date_str = date_str.replace("+0000", "+00:00")
elif "-0000" in date_str:
date_str = date_str.replace("-0000", "+00:00")
# Handle other timezone formats like +0900, -0500, etc.
elif len(date_str) >= 5 and date_str[-5] in "+-" and date_str[-4:].isdigit():
# Insert colon between hours and minutes of timezone
date_str = date_str[:-2] + ":" + date_str[-2:]
try:
date = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
return date.strftime("%Y-%m-%d")
except Exception as e:
logger.warning(f"Error parsing date {date_str}: {e}")
return date_str
def get_issue(self, issue_key: str, expand: str | None = None) -> Document:
"""
Get a single issue with all its details.
Args:
issue_key: The issue key (e.g. 'PROJ-123')
expand: Optional fields to expand
Returns:
Document containing issue content and metadata
"""
try:
issue = self.jira.issue(issue_key, expand=expand)
# Process description and comments
description = self._clean_text(issue["fields"].get("description", ""))
# Get comments
comments = []
if "comment" in issue["fields"]:
for comment in issue["fields"]["comment"]["comments"]:
processed_comment = self._clean_text(comment["body"])
created = self._parse_date(comment["created"])
author = comment["author"].get("displayName", "Unknown")
comments.append(
{
"body": processed_comment,
"created": created,
"author": author,
}
)
# Format created date using new parser
created_date = self._parse_date(issue["fields"]["created"])
# Combine content in a more structured way
content = f"""Issue: {issue_key}
Title: {issue['fields'].get('summary', '')}
Type: {issue['fields']['issuetype']['name']}
Status: {issue['fields']['status']['name']}
Created: {created_date}
Description:
{description}
Comments:
""" + "\n".join([f"{c['created']} - {c['author']}: {c['body']}" for c in comments])
# Streamlined metadata with only essential information
metadata = {
"key": issue_key,
"title": issue["fields"].get("summary", ""),
"type": issue["fields"]["issuetype"]["name"],
"status": issue["fields"]["status"]["name"],
"created_date": created_date,
"priority": issue["fields"].get("priority", {}).get("name", "None"),
"link": f"{self.config.url.rstrip('/')}/browse/{issue_key}",
}
return Document(page_content=content, metadata=metadata)
except Exception as e:
logger.error(f"Error fetching issue {issue_key}: {str(e)}")
raise
def search_issues(
self,
jql: str,
fields: str = "*all",
start: int = 0,
limit: int = 50,
expand: str | None = None,
) -> list[Document]:
"""
Search for issues using JQL.
Args:
jql: JQL query string
fields: Comma-separated string of fields to return
start: Starting index
limit: Maximum results to return
expand: Fields to expand
Returns:
List of Documents containing matching issues
"""
try:
results = self.jira.jql(jql, fields=fields, start=start, limit=limit, expand=expand)
documents = []
for issue in results["issues"]:
# Get full issue details
doc = self.get_issue(issue["key"], expand=expand)
documents.append(doc)
return documents
except Exception as e:
logger.error(f"Error searching issues with JQL {jql}: {str(e)}")
raise
def get_project_issues(self, project_key: str, start: int = 0, limit: int = 50) -> list[Document]:
"""
Get all issues for a project.
Args:
project_key: The project key
start: Starting index
limit: Maximum results to return
Returns:
List of Documents containing project issues
"""
jql = f"project = {project_key} ORDER BY created DESC"
return self.search_issues(jql, start=start, limit=limit)