Python Jira MCP Server
- src
"""
Jira MCP Server implementation.
This module provides a Model Context Protocol server for Jira API integration.
"""
import os
import sys
import asyncio
import json
import logging
from typing import Dict, Any, Callable, Awaitable, Optional, List
# Try to import from the official MCP Python SDK
try:
from mcp.stdio_transport import StdioTransport
from mcp.server import MCPServer
HAS_MCP_SDK = True
except ImportError:
# Fallback to a minimal implementation if the SDK is not available
HAS_MCP_SDK = False
# Import our tool implementations
from src.tools import jql_search, get_issue
from src.tool_schemas import get_tool_schemas
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
filename="jira_mcp.log"
)
logger = logging.getLogger("jira_mcp")
class JiraMCPServer:
"""MCP Server implementation for Jira API integration."""
def __init__(self):
"""Initialize the Jira MCP server."""
# Map tool names to their handler functions
self.tools = {
"jql_search": jql_search,
"get_issue": get_issue,
}
# Load environment variables from .env file if python-dotenv is available
try:
from dotenv import load_dotenv
load_dotenv()
logger.info("Loaded environment from .env file")
except ImportError:
logger.warning("python-dotenv not found, skipping .env loading")
async def handle_tool_call(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""Handle a tool call with the given name and parameters."""
logger.info(f"Handling tool call: {tool_name}")
if tool_name not in self.tools:
logger.error(f"Tool not found: {tool_name}")
return {
"isError": True,
"content": f"Tool not found: {tool_name}"
}
try:
# Call the appropriate tool handler
tool_handler = self.tools[tool_name]
result = await tool_handler(params)
logger.info(f"Tool call completed: {tool_name}")
return result
except Exception as e:
logger.exception(f"Error in tool {tool_name}: {e}")
return {
"isError": True,
"content": f"Error in {tool_name}: {str(e)}"
}
async def start_with_sdk(self):
"""Start the MCP server using the official MCP SDK."""
transport = StdioTransport()
server = MCPServer(transport)
# Register tools with the server
for schema in get_tool_schemas():
name = schema["name"]
async def handler(params, tool_name=name):
return await self.handle_tool_call(tool_name, params)
server.register_tool(schema, handler)
logger.info("Starting Jira MCP server with official SDK")
await server.serve()
async def start_minimal(self):
"""Start a minimal MCP server implementation without the SDK."""
logger.info("Starting minimal Jira MCP server (SDK not available)")
# Read from stdin and write to stdout
while True:
try:
# Read a line from stdin
line = await asyncio.to_thread(sys.stdin.readline)
if not line:
break
# Parse the JSON message
message = json.loads(line)
if "type" not in message:
continue
if message["type"] == "tool_call":
# Handle tool call
tool_name = message.get("name")
params = message.get("parameters", {})
result = await self.handle_tool_call(tool_name, params)
# Send the response
response = {
"type": "tool_call_result",
"id": message.get("id"),
"result": result
}
print(json.dumps(response), flush=True)
elif message["type"] == "discover":
# Handle tool discovery
response = {
"type": "discover_response",
"tools": get_tool_schemas()
}
print(json.dumps(response), flush=True)
except json.JSONDecodeError:
logger.error("Failed to parse JSON message")
except Exception as e:
logger.exception(f"Error processing message: {e}")
async def start(self):
"""Start the Jira MCP server."""
if HAS_MCP_SDK:
await self.start_with_sdk()
else:
await self.start_minimal()
async def main():
"""Main entry point for the Jira MCP server."""
server = JiraMCPServer()
await server.start()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Server stopped by user")
except Exception as e:
logger.exception(f"Unhandled error: {e}")
sys.exit(1)