Python Jira MCP Server

""" Jira tools for Model Context Protocol server. This module defines tools for interacting with Jira API. """ import os import base64 import json from typing import List, Optional, Dict, Any, Union import aiohttp from pydantic import BaseModel, Field class JQLSearchInput(BaseModel): """Input schema for JQL search tool.""" jql: str = Field(..., description="JQL query string") next_page_token: Optional[str] = Field(None, description="Token for pagination") max_results: Optional[int] = Field(None, description="Maximum number of results to return") fields: Optional[List[str]] = Field(None, description="Fields to include in the response") expand: Optional[str] = Field(None, description="Additional data to expand") class GetIssueInput(BaseModel): """Input schema for get_issue tool.""" issue_id_or_key: str = Field(..., description="Issue identifier (ID or key)") fields: Optional[List[str]] = Field(None, description="Fields to return") expand: Optional[str] = Field(None, description="Additional data to expand") properties: Optional[List[str]] = Field(None, description="Properties to include") fail_fast: Optional[bool] = Field(None, description="Fail quickly on errors") class JiraClient: """Client for Jira API.""" def __init__(self): self.base_url = os.environ.get("JIRA_URL") self.email = os.environ.get("JIRA_EMAIL") self.api_token = os.environ.get("JIRA_API_TOKEN") if not all([self.base_url, self.email, self.api_token]): raise ValueError("Missing Jira configuration. Check environment variables.") def get_auth_header(self) -> Dict[str, str]: """Get authorization header for Jira API.""" auth_str = f"{self.email}:{self.api_token}" auth_bytes = auth_str.encode("utf-8") auth_b64 = base64.b64encode(auth_bytes).decode("utf-8") return {"Authorization": f"Basic {auth_b64}"} async def search_issues(self, params: JQLSearchInput) -> Dict[str, Any]: """Search Jira issues using JQL.""" headers = { "Accept": "application/json", "Content-Type": "application/json", **self.get_auth_header() } query_params = {"jql": params.jql} if params.max_results: query_params["maxResults"] = params.max_results if params.fields: query_params["fields"] = ",".join(params.fields) if params.expand: query_params["expand"] = params.expand if params.next_page_token: query_params["startAt"] = params.next_page_token async with aiohttp.ClientSession() as session: async with session.get( f"{self.base_url}/rest/api/3/search", headers=headers, params=query_params ) as response: if response.status != 200: error_text = await response.text() return { "isError": True, "content": f"Jira API error: {response.status} - {error_text}" } data = await response.json() return { "isError": False, "content": data } async def get_issue(self, params: GetIssueInput) -> Dict[str, Any]: """Get Jira issue details by ID or key.""" headers = { "Accept": "application/json", **self.get_auth_header() } query_params = {} if params.fields: query_params["fields"] = ",".join(params.fields) if params.expand: query_params["expand"] = params.expand if params.properties: query_params["properties"] = ",".join(params.properties) if params.fail_fast is not None: query_params["failFast"] = str(params.fail_fast).lower() async with aiohttp.ClientSession() as session: async with session.get( f"{self.base_url}/rest/api/3/issue/{params.issue_id_or_key}", headers=headers, params=query_params ) as response: if response.status != 200: error_text = await response.text() return { "isError": True, "content": f"Jira API error: {response.status} - {error_text}" } data = await response.json() return { "isError": False, "content": data } # Tool handler functions async def jql_search(params: Dict[str, Any]) -> Dict[str, Any]: """Perform JQL search in Jira.""" try: input_params = JQLSearchInput(**params) client = JiraClient() return await client.search_issues(input_params) except Exception as e: return { "isError": True, "content": f"Error in jql_search: {str(e)}" } async def get_issue(params: Dict[str, Any]) -> Dict[str, Any]: """Get Jira issue details.""" try: input_params = GetIssueInput(**params) client = JiraClient() return await client.get_issue(input_params) except Exception as e: return { "isError": True, "content": f"Error in get_issue: {str(e)}" }