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
"""Configuration module for Jira API interactions."""
import logging
import os
from dataclasses import dataclass
from typing import Literal
from ..utils.env import get_custom_headers, is_env_ssl_verify
from ..utils.oauth import (
BYOAccessTokenOAuthConfig,
OAuthConfig,
get_oauth_config_from_env,
)
from ..utils.urls import is_atlassian_cloud_url
@dataclass
class SLAConfig:
"""SLA calculation configuration.
Configures how SLA metrics are calculated, including working hours settings.
"""
default_metrics: list[str] # Default metrics to calculate
working_hours_only: bool = False # Exclude non-working hours
working_hours_start: str = "09:00" # Start of working day (24h format)
working_hours_end: str = "17:00" # End of working day (24h format)
working_days: list[int] | None = None # Working days (1=Mon, 7=Sun)
timezone: str = "UTC" # IANA timezone for calculations
def __post_init__(self) -> None:
"""Set defaults and validate after initialization."""
if self.working_days is None:
self.working_days = [1, 2, 3, 4, 5] # Monday-Friday
else:
# Validate working_days values are in range 1-7
invalid_days = [d for d in self.working_days if d < 1 or d > 7]
if invalid_days:
raise ValueError(
f"Invalid working days: {invalid_days}. Must be 1-7 (Mon-Sun)"
)
@classmethod
def from_env(cls) -> "SLAConfig":
"""Create SLA configuration from environment variables.
Returns:
SLAConfig with values from environment variables
Raises:
ValueError: If working_days contains invalid values
"""
# Default metrics
metrics_str = os.getenv("JIRA_SLA_METRICS", "cycle_time,time_in_status")
default_metrics = [m.strip() for m in metrics_str.split(",")]
# Working hours settings
working_hours_only = os.getenv(
"JIRA_SLA_WORKING_HOURS_ONLY", "false"
).lower() in ("true", "1", "yes")
working_hours_start = os.getenv("JIRA_SLA_WORKING_HOURS_START", "09:00")
working_hours_end = os.getenv("JIRA_SLA_WORKING_HOURS_END", "17:00")
# Working days (1=Monday, 7=Sunday)
working_days_str = os.getenv("JIRA_SLA_WORKING_DAYS", "1,2,3,4,5")
working_days = [int(d.strip()) for d in working_days_str.split(",")]
# Validate working_days
invalid_days = [d for d in working_days if d < 1 or d > 7]
if invalid_days:
raise ValueError(
f"Invalid JIRA_SLA_WORKING_DAYS: {invalid_days}. Must be 1-7 (Mon-Sun)"
)
# Timezone
timezone = os.getenv("JIRA_SLA_TIMEZONE", "UTC")
return cls(
default_metrics=default_metrics,
working_hours_only=working_hours_only,
working_hours_start=working_hours_start,
working_hours_end=working_hours_end,
working_days=working_days,
timezone=timezone,
)
@dataclass
class JiraConfig:
"""Jira API configuration.
Handles authentication for Jira Cloud and Server/Data Center:
- Cloud: username/API token (basic auth) or OAuth 2.0 (3LO)
- Server/DC: personal access token or basic auth
"""
url: str # Base URL for Jira
auth_type: Literal["basic", "pat", "oauth"] # Authentication type
username: str | None = None # Email or username (Cloud)
api_token: str | None = None # API token (Cloud)
personal_token: str | None = None # Personal access token (Server/DC)
oauth_config: OAuthConfig | BYOAccessTokenOAuthConfig | None = None
ssl_verify: bool = True # Whether to verify SSL certificates
projects_filter: str | None = None # List of project keys to filter searches
http_proxy: str | None = None # HTTP proxy URL
https_proxy: str | None = None # HTTPS proxy URL
no_proxy: str | None = None # Comma-separated list of hosts to bypass proxy
socks_proxy: str | None = None # SOCKS proxy URL (optional)
custom_headers: dict[str, str] | None = None # Custom HTTP headers
disable_jira_markup_translation: bool = (
False # Disable automatic markup translation between formats
)
client_cert: str | None = None # Client certificate file path (.pem)
client_key: str | None = None # Client private key file path (.pem)
client_key_password: str | None = None # Password for encrypted private key
sla_config: SLAConfig | None = None # Optional SLA configuration
timeout: int = 75 # Connection timeout in seconds
@property
def is_cloud(self) -> bool:
"""Check if this is a cloud instance.
Returns:
True if this is a cloud instance (atlassian.net), False otherwise.
Localhost URLs are always considered non-cloud (Server/Data Center).
"""
# OAuth with cloud_id uses api.atlassian.com which is always Cloud
if (
self.auth_type == "oauth"
and self.oauth_config
and self.oauth_config.cloud_id
):
return True
# DC OAuth has base_url but no cloud_id — not Cloud
if (
self.auth_type == "oauth"
and self.oauth_config
and hasattr(self.oauth_config, "base_url")
and self.oauth_config.base_url
and not self.oauth_config.cloud_id
):
return False
# For other auth types, check the URL
return is_atlassian_cloud_url(self.url) if self.url else False
@property
def verify_ssl(self) -> bool:
"""Compatibility property for old code.
Returns:
The ssl_verify value
"""
return self.ssl_verify
@classmethod
def from_env(cls) -> "JiraConfig":
"""Create configuration from environment variables.
Returns:
JiraConfig with values from environment variables
Raises:
ValueError: If required environment variables are missing or invalid
"""
url = os.getenv("JIRA_URL")
if not url and not os.getenv("ATLASSIAN_OAUTH_ENABLE"):
error_msg = (
"Missing required JIRA_URL environment variable. "
"Set JIRA_URL to your Jira base URL, for example "
"https://your-company.atlassian.net"
)
raise ValueError(error_msg)
# Determine authentication type based on available environment variables
username = os.getenv("JIRA_USERNAME")
api_token = os.getenv("JIRA_API_TOKEN")
personal_token = os.getenv("JIRA_PERSONAL_TOKEN")
# Check for OAuth configuration (pass service info for DC detection)
oauth_config = get_oauth_config_from_env(service_url=url, service_type="jira")
auth_type = None
# Use the shared utility function directly
is_cloud = is_atlassian_cloud_url(url) if url else False
if is_cloud:
# Cloud: OAuth takes priority, then basic auth
if oauth_config:
auth_type = "oauth"
elif username and api_token:
auth_type = "basic"
else:
missing_fields: list[str] = []
if not username:
missing_fields.append("JIRA_USERNAME")
if not api_token:
missing_fields.append("JIRA_API_TOKEN")
missing_fields_text = ", ".join(missing_fields)
error_msg = (
"Cloud authentication requires JIRA_USERNAME and "
"JIRA_API_TOKEN, or OAuth configuration "
"(set ATLASSIAN_OAUTH_ENABLE=true for user-provided tokens). "
"Jira Cloud authentication is incomplete. Missing: "
f"{missing_fields_text}. "
"Set JIRA_USERNAME and JIRA_API_TOKEN, or enable OAuth with "
"ATLASSIAN_OAUTH_ENABLE=true."
)
raise ValueError(error_msg)
else: # Server/Data Center
# Server/DC: PAT takes priority over OAuth (fixes #824)
if personal_token:
if oauth_config:
logger = logging.getLogger("mcp-atlassian.jira.config")
logger.warning(
"Both PAT and OAuth configured for Server/DC. Using PAT."
)
auth_type = "pat"
elif oauth_config:
auth_type = "oauth"
elif username and api_token:
auth_type = "basic"
else:
error_msg = (
"Server/Data Center authentication requires "
"JIRA_PERSONAL_TOKEN or JIRA_USERNAME and JIRA_API_TOKEN. "
"Jira Server/Data Center authentication is incomplete. "
"Set JIRA_PERSONAL_TOKEN, or set both JIRA_USERNAME and "
"JIRA_API_TOKEN."
)
raise ValueError(error_msg)
# SSL verification (for Server/DC)
ssl_verify = is_env_ssl_verify("JIRA_SSL_VERIFY")
# Get the projects filter if provided
projects_filter = os.getenv("JIRA_PROJECTS_FILTER")
# Proxy settings
http_proxy = os.getenv("JIRA_HTTP_PROXY", os.getenv("HTTP_PROXY"))
https_proxy = os.getenv("JIRA_HTTPS_PROXY", os.getenv("HTTPS_PROXY"))
no_proxy = os.getenv("JIRA_NO_PROXY", os.getenv("NO_PROXY"))
socks_proxy = os.getenv("JIRA_SOCKS_PROXY", os.getenv("SOCKS_PROXY"))
# Custom headers - service-specific only
custom_headers = get_custom_headers("JIRA_CUSTOM_HEADERS")
# Markup translation setting
disable_jira_markup_translation = (
os.getenv("DISABLE_JIRA_MARKUP_TRANSLATION", "false").lower() == "true"
)
# Client certificate settings
client_cert = os.getenv("JIRA_CLIENT_CERT")
client_key = os.getenv("JIRA_CLIENT_KEY")
client_key_password = os.getenv("JIRA_CLIENT_KEY_PASSWORD")
# Timeout setting
timeout = 75 # Default timeout
if os.getenv("JIRA_TIMEOUT") and os.getenv("JIRA_TIMEOUT", "").isdigit():
timeout = int(os.getenv("JIRA_TIMEOUT", "75"))
return cls(
url=url or "",
auth_type=auth_type,
username=username,
api_token=api_token,
personal_token=personal_token,
oauth_config=oauth_config,
ssl_verify=ssl_verify,
projects_filter=projects_filter,
http_proxy=http_proxy,
https_proxy=https_proxy,
no_proxy=no_proxy,
socks_proxy=socks_proxy,
custom_headers=custom_headers,
disable_jira_markup_translation=disable_jira_markup_translation,
client_cert=client_cert,
client_key=client_key,
client_key_password=client_key_password,
timeout=timeout,
)
def is_auth_configured(self) -> bool:
"""Check if the current authentication configuration is complete and valid for making API calls.
Returns:
bool: True if authentication is fully configured, False otherwise.
"""
logger = logging.getLogger("mcp-atlassian.jira.config")
if self.auth_type == "oauth":
if self.oauth_config:
# Minimal OAuth (user-provided tokens mode)
if isinstance(self.oauth_config, OAuthConfig):
if (
not self.oauth_config.client_id
and not self.oauth_config.client_secret
):
logger.debug(
"Minimal OAuth config detected - "
"expecting user-provided tokens via headers"
)
return True
# DC OAuth: needs client_id + client_secret (no cloud_id needed)
if hasattr(self.oauth_config, "is_data_center"):
if self.oauth_config.is_data_center:
return bool(
self.oauth_config.client_id
and self.oauth_config.client_secret
)
# Cloud OAuth: full set required
if (
self.oauth_config.client_id
and self.oauth_config.client_secret
and self.oauth_config.redirect_uri
and self.oauth_config.scope
and self.oauth_config.cloud_id
):
return True
# BYO Access Token mode
elif isinstance(self.oauth_config, BYOAccessTokenOAuthConfig):
if self.oauth_config.access_token:
# DC BYO: access_token is enough
if hasattr(self.oauth_config, "is_data_center"):
if self.oauth_config.is_data_center:
return True
# Cloud BYO: needs cloud_id + access_token
if self.oauth_config.cloud_id:
return True
logger.warning("Incomplete OAuth configuration detected")
return False
elif self.auth_type == "pat":
return bool(self.personal_token)
elif self.auth_type == "basic":
return bool(self.username and self.api_token)
logger.warning(
f"Unknown or unsupported auth_type: {self.auth_type} in JiraConfig"
)
return False