Skip to main content
Glama

MCP Atlassian

by ArconixForge
attachments.py9.72 kB
"""Attachment operations for Jira API.""" import logging import os from pathlib import Path from typing import Any from ..models.jira import JiraAttachment from .client import JiraClient from .protocols import AttachmentsOperationsProto # Configure logging logger = logging.getLogger("mcp-jira") class AttachmentsMixin(JiraClient, AttachmentsOperationsProto): """Mixin for Jira attachment operations.""" def download_attachment(self, url: str, target_path: str) -> bool: """ Download a Jira attachment to the specified path. Args: url: The URL of the attachment to download target_path: The path where the attachment should be saved Returns: True if successful, False otherwise """ if not url: logger.error("No URL provided for attachment download") return False try: # Convert to absolute path if relative if not os.path.isabs(target_path): target_path = os.path.abspath(target_path) logger.info(f"Downloading attachment from {url} to {target_path}") # Create the directory if it doesn't exist os.makedirs(os.path.dirname(target_path), exist_ok=True) # Use the Jira session to download the file response = self.jira._session.get(url, stream=True) response.raise_for_status() # Write the file to disk with open(target_path, "wb") as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) # Verify the file was created if os.path.exists(target_path): file_size = os.path.getsize(target_path) logger.info( f"Successfully downloaded attachment to {target_path} (size: {file_size} bytes)" ) return True else: logger.error(f"File was not created at {target_path}") return False except Exception as e: logger.error(f"Error downloading attachment: {str(e)}") return False def download_issue_attachments( self, issue_key: str, target_dir: str ) -> dict[str, Any]: """ Download all attachments for a Jira issue. Args: issue_key: The Jira issue key (e.g., 'PROJ-123') target_dir: The directory where attachments should be saved Returns: A dictionary with download results """ # Convert to absolute path if relative if not os.path.isabs(target_dir): target_dir = os.path.abspath(target_dir) logger.info( f"Downloading attachments for {issue_key} to directory: {target_dir}" ) # Create the target directory if it doesn't exist target_path = Path(target_dir) target_path.mkdir(parents=True, exist_ok=True) # Get the issue with attachments logger.info(f"Fetching issue {issue_key} with attachments") issue_data = self.jira.issue(issue_key, fields="attachment") if not isinstance(issue_data, dict): msg = f"Unexpected return value type from `jira.issue`: {type(issue_data)}" logger.error(msg) raise TypeError(msg) if "fields" not in issue_data: logger.error(f"Could not retrieve issue {issue_key}") return {"success": False, "error": f"Could not retrieve issue {issue_key}"} # Process attachments attachments = [] results = [] # Extract attachments from the API response attachment_data = issue_data.get("fields", {}).get("attachment", []) if not attachment_data: return { "success": True, "message": f"No attachments found for issue {issue_key}", "downloaded": [], "failed": [], } # Create JiraAttachment objects for each attachment for attachment in attachment_data: if isinstance(attachment, dict): attachments.append(JiraAttachment.from_api_response(attachment)) # Download each attachment downloaded = [] failed = [] for attachment in attachments: if not attachment.url: logger.warning(f"No URL for attachment {attachment.filename}") failed.append( {"filename": attachment.filename, "error": "No URL available"} ) continue # Create a safe filename safe_filename = Path(attachment.filename).name file_path = target_path / safe_filename # Download the attachment success = self.download_attachment(attachment.url, str(file_path)) if success: downloaded.append( { "filename": attachment.filename, "path": str(file_path), "size": attachment.size, } ) else: failed.append( {"filename": attachment.filename, "error": "Download failed"} ) return { "success": True, "issue_key": issue_key, "total": len(attachments), "downloaded": downloaded, "failed": failed, } def upload_attachment(self, issue_key: str, file_path: str) -> dict[str, Any]: """ Upload a single attachment to a Jira issue. Args: issue_key: The Jira issue key (e.g., 'PROJ-123') file_path: The path to the file to upload Returns: A dictionary with upload result information """ if not issue_key: logger.error("No issue key provided for attachment upload") return {"success": False, "error": "No issue key provided"} if not file_path: logger.error("No file path provided for attachment upload") return {"success": False, "error": "No file path provided"} try: # Convert to absolute path if relative if not os.path.isabs(file_path): file_path = os.path.abspath(file_path) # Check if file exists if not os.path.exists(file_path): logger.error(f"File not found: {file_path}") return {"success": False, "error": f"File not found: {file_path}"} logger.info(f"Uploading attachment from {file_path} to issue {issue_key}") # Use the Jira API to upload the file filename = os.path.basename(file_path) with open(file_path, "rb") as file: attachment = self.jira.add_attachment( issue_key=issue_key, filename=file_path ) if attachment: file_size = os.path.getsize(file_path) logger.info( f"Successfully uploaded attachment {filename} to {issue_key} (size: {file_size} bytes)" ) return { "success": True, "issue_key": issue_key, "filename": filename, "size": file_size, "id": attachment.get("id") if isinstance(attachment, dict) else None, } else: logger.error(f"Failed to upload attachment {filename} to {issue_key}") return { "success": False, "error": f"Failed to upload attachment {filename} to {issue_key}", } except Exception as e: error_msg = str(e) logger.error(f"Error uploading attachment: {error_msg}") return {"success": False, "error": error_msg} def upload_attachments( self, issue_key: str, file_paths: list[str] ) -> dict[str, Any]: """ Upload multiple attachments to a Jira issue. Args: issue_key: The Jira issue key (e.g., 'PROJ-123') file_paths: List of paths to files to upload Returns: A dictionary with upload results """ if not issue_key: logger.error("No issue key provided for attachment upload") return {"success": False, "error": "No issue key provided"} if not file_paths: logger.error("No file paths provided for attachment upload") return {"success": False, "error": "No file paths provided"} logger.info(f"Uploading {len(file_paths)} attachments to issue {issue_key}") # Upload each attachment uploaded = [] failed = [] for file_path in file_paths: result = self.upload_attachment(issue_key, file_path) if result.get("success"): uploaded.append( { "filename": result.get("filename"), "size": result.get("size"), "id": result.get("id"), } ) else: failed.append( { "filename": os.path.basename(file_path), "error": result.get("error"), } ) return { "success": True, "issue_key": issue_key, "total": len(file_paths), "uploaded": uploaded, "failed": failed, }

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

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