Skip to main content
Glama

MCP Atlassian

by uchinx
MIT License
  • Apple
  • Linux
client.py11.5 kB
"""Base client module for Jira API interactions.""" import logging import os from typing import Any, Literal from atlassian import Jira from requests import Session from mcp_atlassian.exceptions import MCPAtlassianAuthenticationError from mcp_atlassian.preprocessing import JiraPreprocessor from mcp_atlassian.utils.logging import ( get_masked_session_headers, log_config_param, mask_sensitive, ) from mcp_atlassian.utils.oauth import configure_oauth_session from mcp_atlassian.utils.ssl import configure_ssl_verification from .config import JiraConfig # Configure logging logger = logging.getLogger("mcp-jira") class JiraClient: """Base client for Jira API interactions.""" _field_ids_cache: list[dict[str, Any]] | None _current_user_account_id: str | None config: JiraConfig preprocessor: JiraPreprocessor def __init__(self, config: JiraConfig | None = None) -> None: """Initialize the Jira client with configuration options. Args: config: Optional configuration object (will use env vars if not provided) Raises: ValueError: If configuration is invalid or required credentials are missing MCPAtlassianAuthenticationError: If OAuth authentication fails """ # Load configuration from environment variables if not provided self.config = config or JiraConfig.from_env() # Initialize the Jira client based on auth type if self.config.auth_type == "oauth": if not self.config.oauth_config or not self.config.oauth_config.cloud_id: error_msg = "OAuth authentication requires a valid cloud_id" raise ValueError(error_msg) # Create a session for OAuth session = Session() # Configure the session with OAuth authentication if not configure_oauth_session(session, self.config.oauth_config): error_msg = "Failed to configure OAuth session" raise MCPAtlassianAuthenticationError(error_msg) # The Jira API URL with OAuth is different api_url = ( f"https://api.atlassian.com/ex/jira/{self.config.oauth_config.cloud_id}" ) # Initialize Jira with the session self.jira = Jira( url=api_url, session=session, cloud=True, # OAuth is only for Cloud verify_ssl=self.config.ssl_verify, ) elif self.config.auth_type == "pat": logger.debug( f"Initializing Jira client with Token (PAT) auth. " f"URL: {self.config.url}, " f"Token (masked): {mask_sensitive(str(self.config.personal_token))}" ) self.jira = Jira( url=self.config.url, token=self.config.personal_token, cloud=self.config.is_cloud, verify_ssl=self.config.ssl_verify, ) else: # basic auth logger.debug( f"Initializing Jira client with Basic auth. " f"URL: {self.config.url}, Username: {self.config.username}, " f"API Token present: {bool(self.config.api_token)}, " f"Is Cloud: {self.config.is_cloud}" ) self.jira = Jira( url=self.config.url, username=self.config.username, password=self.config.api_token, cloud=self.config.is_cloud, verify_ssl=self.config.ssl_verify, ) logger.debug( f"Jira client initialized. Session headers (Authorization masked): " f"{get_masked_session_headers(dict(self.jira._session.headers))}" ) # Configure SSL verification using the shared utility configure_ssl_verification( service_name="Jira", url=self.config.url, session=self.jira._session, ssl_verify=self.config.ssl_verify, ) # Proxy configuration proxies = {} if self.config.http_proxy: proxies["http"] = self.config.http_proxy if self.config.https_proxy: proxies["https"] = self.config.https_proxy if self.config.socks_proxy: proxies["socks"] = self.config.socks_proxy if proxies: self.jira._session.proxies.update(proxies) for k, v in proxies.items(): log_config_param( logger, "Jira", f"{k.upper()}_PROXY", v, sensitive=True ) if self.config.no_proxy and isinstance(self.config.no_proxy, str): os.environ["NO_PROXY"] = self.config.no_proxy log_config_param(logger, "Jira", "NO_PROXY", self.config.no_proxy) # Apply custom headers if configured if self.config.custom_headers: self._apply_custom_headers() # Initialize the text preprocessor for text processing capabilities self.preprocessor = JiraPreprocessor(base_url=self.config.url) self._field_ids_cache = None self._current_user_account_id = None # Test authentication during initialization (in debug mode only) if logger.isEnabledFor(logging.DEBUG): try: self._validate_authentication() except MCPAtlassianAuthenticationError: logger.warning( "Authentication validation failed during client initialization - " "continuing anyway" ) def _validate_authentication(self) -> None: """Validate authentication by making a simple API call.""" try: logger.debug( "Testing Jira authentication by retrieving current user info..." ) current_user = self.jira.myself() if current_user: logger.info( f"Jira authentication successful. " f"Current user: {current_user.get('displayName', 'Unknown')} " f"({current_user.get('emailAddress', 'No email')})" ) else: logger.warning( "Jira authentication test returned empty user info - " "this may indicate an issue" ) except Exception as e: error_msg = f"Jira authentication validation failed: {e}" logger.error(error_msg) logger.debug( f"Authentication headers during failure: " f"{get_masked_session_headers(dict(self.jira._session.headers))}" ) raise MCPAtlassianAuthenticationError(error_msg) from e def _apply_custom_headers(self) -> None: """Apply custom headers to the Jira session.""" if not self.config.custom_headers: return logger.debug( f"Applying {len(self.config.custom_headers)} custom headers to Jira session" ) for header_name, header_value in self.config.custom_headers.items(): self.jira._session.headers[header_name] = header_value logger.debug(f"Applied custom header: {header_name}") def _clean_text(self, text: str) -> str: """Clean text content by: 1. Processing user mentions and links 2. Converting HTML/wiki markup to markdown Args: text: Text to clean Returns: Cleaned text """ if not text: return "" # Otherwise create a temporary one _ = self.config.url if hasattr(self, "config") else "" return self.preprocessor.clean_jira_text(text) def _markdown_to_jira(self, markdown_text: str) -> str: """ Convert Markdown syntax to Jira markup syntax. Args: markdown_text: Text in Markdown format Returns: Text in Jira markup format """ if not markdown_text: return "" # Use the shared preprocessor if available if hasattr(self, "preprocessor"): return self.preprocessor.markdown_to_jira(markdown_text) # Otherwise create a temporary one _ = self.config.url if hasattr(self, "config") else "" return self.preprocessor.markdown_to_jira(markdown_text) def get_paged( self, method: Literal["get", "post"], url: str, params_or_json: dict | None = None, *, absolute: bool = False, ) -> list[dict]: """ Repeatly fetch paged data from Jira API using `nextPageToken` to paginate. Args: method: The HTTP method to use url: The URL to retrieve data from params_or_json: Optional query parameters or JSON data to send absolute: Whether to use absolute URL Returns: List of requested json data Raises: ValueError: If using paged request on non-cloud Jira """ if not self.config.is_cloud: raise ValueError( "Paged requests are only available for Jira Cloud platform" ) all_results: list[dict] = [] current_data = params_or_json or {} while True: if method == "get": api_result = self.jira.get( path=url, params=current_data, absolute=absolute ) else: api_result = self.jira.post( path=url, json=current_data, absolute=absolute ) if not isinstance(api_result, dict): error_message = f"API result is not a dictionary: {api_result}" logger.error(error_message) raise ValueError(error_message) # Extract values from response all_results.append(api_result) # Check if this is the last page if "nextPageToken" not in api_result: break # Update for next iteration current_data["nextPageToken"] = api_result["nextPageToken"] return all_results def create_version( self, project: str, name: str, start_date: str = None, release_date: str = None, description: str = None, ) -> dict[str, Any]: """ Create a new version in a Jira project. Args: project: The project key (e.g., 'PROJ') name: The name of the version start_date: The start date (YYYY-MM-DD, optional) release_date: The release date (YYYY-MM-DD, optional) description: Description of the version (optional) Returns: The created version object as returned by Jira """ payload = {"project": project, "name": name} if start_date: payload["startDate"] = start_date if release_date: payload["releaseDate"] = release_date if description: payload["description"] = description logger.info(f"Creating Jira version: {payload}") result = self.jira.post("/rest/api/3/version", json=payload) if not isinstance(result, dict): error_message = f"Unexpected response from Jira API: {result}" raise ValueError(error_message) return result

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