#
tokens: 20121/50000 18/18 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env.example
├── .gitignore
├── dist
│   ├── __tests__
│   │   ├── server.test.d.ts
│   │   └── server.test.js
│   ├── index.d.ts
│   ├── index.js
│   ├── schemas.d.ts
│   ├── schemas.js
│   ├── server.d.ts
│   ├── server.js
│   ├── types.d.ts
│   └── types.js
├── LICENSE
├── node_modules
│   ├── .package-lock.json
│   ├── @modelcontextprotocol
│   │   └── sdk
│   │       ├── LICENSE
│   │       ├── package.json
│   │       └── README.md
│   ├── bytes
│   │   ├── History.md
│   │   ├── index.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   └── Readme.md
│   ├── content-type
│   │   ├── HISTORY.md
│   │   ├── index.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   └── README.md
│   ├── depd
│   │   ├── History.md
│   │   ├── index.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   └── Readme.md
│   ├── http-errors
│   │   ├── HISTORY.md
│   │   ├── index.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   └── README.md
│   ├── iconv-lite
│   │   ├── .github
│   │   │   └── dependabot.yml
│   │   ├── Changelog.md
│   │   ├── encodings
│   │   │   ├── dbcs-codec.js
│   │   │   ├── dbcs-data.js
│   │   │   ├── index.js
│   │   │   ├── internal.js
│   │   │   ├── sbcs-codec.js
│   │   │   ├── sbcs-data-generated.js
│   │   │   ├── sbcs-data.js
│   │   │   ├── tables
│   │   │   │   ├── big5-added.json
│   │   │   │   ├── cp936.json
│   │   │   │   ├── cp949.json
│   │   │   │   ├── cp950.json
│   │   │   │   ├── eucjp.json
│   │   │   │   ├── gb18030-ranges.json
│   │   │   │   ├── gbk-added.json
│   │   │   │   └── shiftjis.json
│   │   │   ├── utf16.js
│   │   │   ├── utf32.js
│   │   │   └── utf7.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   └── README.md
│   ├── inherits
│   │   ├── inherits_browser.js
│   │   ├── inherits.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   └── README.md
│   ├── raw-body
│   │   ├── HISTORY.md
│   │   ├── index.d.ts
│   │   ├── index.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   ├── README.md
│   │   └── SECURITY.md
│   ├── safer-buffer
│   │   ├── dangerous.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   ├── Porting-Buffer.md
│   │   ├── Readme.md
│   │   ├── safer.js
│   │   └── tests.js
│   ├── setprototypeof
│   │   ├── index.d.ts
│   │   ├── index.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   ├── README.md
│   │   └── test
│   │       └── index.js
│   ├── statuses
│   │   ├── codes.json
│   │   ├── HISTORY.md
│   │   ├── index.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   └── README.md
│   ├── toidentifier
│   │   ├── HISTORY.md
│   │   ├── index.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   └── README.md
│   ├── unpipe
│   │   ├── HISTORY.md
│   │   ├── index.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   └── README.md
│   └── zod
│       ├── index.d.ts
│       ├── LICENSE
│       ├── package.json
│       └── README.md
├── package-lock.json
├── pyproject.toml
├── QUICKSTART.md
├── README.md
├── SECURITY.md
├── setup.py
├── src
│   └── mcp_jira
│       ├── __init__.py
│       ├── __main__.py
│       ├── config.py
│       ├── jira_client.py
│       ├── mcp_protocol.py
│       ├── simple_mcp_server.py
│       └── types.py
└── tests
    ├── __init__.py
    ├── conftest.py
    ├── test_jira_client.py
    └── test_simple_mcp_server.py
```

# Files

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
 1 | # Jira Configuration
 2 | JIRA_URL=https://your-domain.atlassian.net
 3 | [email protected]
 4 | JIRA_API_TOKEN=your_api_token
 5 | PROJECT_KEY=PROJ
 6 | DEFAULT_BOARD_ID=123
 7 | 
 8 | # Application Settings
 9 | DEBUG_MODE=false
10 | LOG_LEVEL=INFO
11 | 
12 | # Optional: Sprint Defaults
13 | DEFAULT_SPRINT_LENGTH=14
14 | STORY_POINTS_FIELD=customfield_10026
15 | MAX_SPRINT_ITEMS=50
16 | 
17 | # Performance Settings
18 | JIRA_REQUEST_TIMEOUT=30
19 | CACHE_TTL=300
20 | MAX_CONCURRENT_REQUESTS=10
21 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | ### Python ###
 2 | __pycache__/
 3 | *.py[cod]
 4 | *$py.class
 5 | *.so
 6 | .Python
 7 | env/
 8 | build/
 9 | develop-eggs/
10 | dist/
11 | downloads/
12 | eggs/
13 | .eggs/
14 | lib/
15 | lib64/
16 | parts/
17 | sdist/
18 | var/
19 | wheels/
20 | *.egg-info/
21 | .installed.cfg
22 | *.egg
23 | 
24 | # Virtual Environment
25 | venv/
26 | ENV/
27 | 
28 | # IDE
29 | .idea/
30 | .vscode/
31 | *.swp
32 | *.swo
33 | 
34 | # Environment variables
35 | .env
36 | .env.local
37 | 
38 | # Logs
39 | *.log
40 | 
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | .hypothesis/
52 | .pytest_cache/
53 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Jira Integration
  2 | 
  3 | A simple Model Context Protocol (MCP) server for Jira that allows LLMs to act as project managers and personal assistants for teams using Jira.
  4 | 
  5 | ## Features
  6 | 
  7 | ### Core MCP Tools
  8 | - **create_issue** - Create new Jira issues with proper formatting
  9 | - **search_issues** - Search issues using JQL with smart formatting
 10 | - **get_sprint_status** - Get comprehensive sprint progress reports
 11 | - **get_team_workload** - Analyze team member workloads and capacity
 12 | - **generate_standup_report** - Generate daily standup reports automatically
 13 | 
 14 | ### Project Management Capabilities
 15 | - Sprint progress tracking with visual indicators
 16 | - Team workload analysis and capacity planning
 17 | - Automated daily standup report generation
 18 | - Issue creation with proper prioritization
 19 | - Smart search and filtering of issues
 20 | 
 21 | ## Requirements
 22 | 
 23 | - Python 3.8 or higher
 24 | - Jira account with API token
 25 | - MCP-compatible client (like Claude Desktop)
 26 | 
 27 | ## Quick Setup
 28 | 
 29 | 1. **Clone and install**:
 30 | ```bash
 31 | cd mcp-jira
 32 | pip install -e .
 33 | ```
 34 | 
 35 | 2. **Configure Jira credentials** in `.env`:
 36 | ```env
 37 | JIRA_URL=https://your-domain.atlassian.net
 38 | [email protected]
 39 | JIRA_API_TOKEN=your_api_token
 40 | PROJECT_KEY=PROJ
 41 | DEFAULT_BOARD_ID=123
 42 | ```
 43 | 
 44 | 3. **Run the MCP server**:
 45 | ```bash
 46 | python -m mcp_jira.simple_mcp_server
 47 | ```
 48 | 
 49 | ## Usage Examples
 50 | 
 51 | ### Creating Issues
 52 | "Create a high priority bug for the login system not working properly"
 53 | - Auto-assigns proper issue type, priority, and formatting
 54 | 
 55 | ### Sprint Management
 56 | "What's our current sprint status?"
 57 | - Gets comprehensive progress report with metrics and visual indicators
 58 | 
 59 | ### Team Management
 60 | "Show me the team workload for john.doe, jane.smith, mike.wilson"
 61 | - Analyzes capacity and provides workload distribution
 62 | 
 63 | ### Daily Standups
 64 | "Generate today's standup report"
 65 | - Creates formatted report with completed, in-progress, and blocked items
 66 | 
 67 | ## MCP Integration
 68 | 
 69 | ### With Claude Desktop
 70 | Add to your `claude_desktop_config.json`:
 71 | ```json
 72 | {
 73 |   "mcpServers": {
 74 |     "mcp-jira": {
 75 |       "command": "python",
 76 |       "args": ["-m", "mcp_jira.simple_mcp_server"],
 77 |       "cwd": "/path/to/mcp-jira"
 78 |     }
 79 |   }
 80 | }
 81 | ```
 82 | 
 83 | ### With Other MCP Clients
 84 | The server follows the standard MCP protocol and works with any MCP-compatible client.
 85 | 
 86 | ## Configuration
 87 | 
 88 | ### Required Environment Variables
 89 | - `JIRA_URL` - Your Jira instance URL
 90 | - `JIRA_USERNAME` - Your Jira username/email
 91 | - `JIRA_API_TOKEN` - Your Jira API token
 92 | - `PROJECT_KEY` - Default project key for operations
 93 | 
 94 | ### Optional Settings
 95 | - `DEFAULT_BOARD_ID` - Default board for sprint operations
 96 | - `DEBUG_MODE` - Enable debug logging (default: false)
 97 | - `LOG_LEVEL` - Logging level (default: INFO)
 98 | 
 99 | ## Getting Jira API Token
100 | 
101 | 1. Go to [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens)
102 | 2. Click "Create API token"
103 | 3. Give it a name and copy the token
104 | 4. Use your email as username and the token as password
105 | 
106 | ## Architecture
107 | 
108 | This implementation prioritizes simplicity:
109 | - **Single MCP server file** - All tools in one place
110 | - **Standard MCP protocol** - Uses official MCP SDK
111 | - **Rich formatting** - Provides beautiful, readable reports
112 | - **Error handling** - Graceful handling of Jira API issues
113 | - **Async support** - Fast and responsive operations
114 | 
115 | ## Troubleshooting
116 | 
117 | ### Common Issues
118 | 
119 | 1. **"No active sprint found"**
120 |    - Make sure your board has an active sprint
121 |    - Check that `DEFAULT_BOARD_ID` is set correctly
122 | 
123 | 2. **Authentication errors**
124 |    - Verify your API token is correct
125 |    - Check that your username is your email address
126 | 
127 | 3. **Permission errors**
128 |    - Ensure your Jira user has appropriate project permissions
129 |    - Check that the project key exists and you have access
130 | 
131 | ### Debug Mode
132 | Set `DEBUG_MODE=true` in your `.env` file for detailed logging.
133 | 
134 | ## Contributing
135 | 
136 | 1. Fork the repository
137 | 2. Make your changes
138 | 3. Test with your Jira instance
139 | 4. Submit a pull request
140 | 
141 | ## License
142 | 
143 | MIT License - see LICENSE file
144 | 
```

--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Security Policy
 2 | 
 3 | ## Supported Versions
 4 | 
 5 | We release patches for security vulnerabilities. Which versions are eligible for receiving such patches depends on the CVSS v3.0 Rating:
 6 | 
 7 | | Version | Supported          |
 8 | | ------- | ------------------ |
 9 | | 1.x.x   | :white_check_mark: |
10 | 
11 | ## Reporting a Vulnerability
12 | 
13 | Please report (suspected) security vulnerabilities to [email protected]. You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
14 | 
15 | ## Security Measures
16 | 
17 | 1. **Authentication**: All JIRA API calls require proper authentication using API tokens
18 | 2. **Data Protection**: Sensitive data like API tokens should be provided via environment variables
19 | 3. **Input Validation**: All inputs are validated before being used in JIRA API calls
20 | 4. **Error Handling**: Errors are caught and handled appropriately without exposing sensitive information
```

--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/src/mcp_jira/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """Model Context Protocol server for Jira with Scrum Master capabilities"""
2 | 
3 | from importlib.metadata import version
4 | 
5 | __version__ = version("mcp-jira")
6 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "mcp-jira"
 3 | version = "0.1.0"
 4 | description = "Model Context Protocol server for Jira with Scrum Master capabilities"
 5 | authors = [{name = "Warzuponus"}]
 6 | dependencies = [
 7 |     "mcp>=1.0.0",
 8 |     "jira>=3.5.1",
 9 |     "python-dotenv>=0.19.0",
10 |     "pydantic>=2.0.0",
11 |     "pydantic-settings>=2.0.0",
12 |     "aiohttp>=3.8.0"
13 | ]
14 | 
15 | [build-system]
16 | requires = ["hatchling"]
17 | build-backend = "hatchling.build"
18 | 
19 | [tool.pytest.ini_options]
20 | testpaths = ["tests"]
21 | 
```

--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """
 3 | Setup script for mcp-jira.
 4 | """
 5 | 
 6 | from setuptools import setup, find_packages
 7 | 
 8 | with open("README.md", "r", encoding="utf-8") as fh:
 9 |     long_description = fh.read()
10 | 
11 | setup(
12 |     name="mcp-jira",
13 |     version="0.1.0",
14 |     author="Warzuponus",
15 |     description="Model Context Protocol server for Jira with project management capabilities",
16 |     long_description=long_description,
17 |     long_description_content_type="text/markdown",
18 |     url="https://github.com/warzuponus/mcp-jira",
19 |     packages=find_packages(where="src"),
20 |     package_dir={"": "src"},
21 |     classifiers=[
22 |         "Development Status :: 3 - Alpha",
23 |         "Intended Audience :: Developers",
24 |         "License :: OSI Approved :: MIT License",
25 |         "Operating System :: OS Independent",
26 |         "Programming Language :: Python :: 3",
27 |         "Programming Language :: Python :: 3.8",
28 |         "Programming Language :: Python :: 3.9",
29 |         "Programming Language :: Python :: 3.10",
30 |         "Programming Language :: Python :: 3.11",
31 |         "Programming Language :: Python :: 3.12",
32 |     ],
33 |     python_requires=">=3.8",
34 |     install_requires=[
35 |         "mcp>=1.0.0",
36 |         "jira>=3.5.1",
37 |         "python-dotenv>=0.19.0",
38 |         "pydantic>=2.0.0",
39 |         "pydantic-settings>=2.0.0",
40 |         "aiohttp>=3.8.0",
41 |     ],
42 |     entry_points={
43 |         "console_scripts": [
44 |             "mcp-jira=mcp_jira.simple_mcp_server:main",
45 |         ],
46 |     },
47 |     include_package_data=True,
48 |     zip_safe=False,
49 | ) 
```

--------------------------------------------------------------------------------
/src/mcp_jira/__main__.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """
 3 | Main entry point for mcp-jira.
 4 | Allows running with `python -m mcp_jira`.
 5 | """
 6 | 
 7 | import asyncio
 8 | import sys
 9 | import logging
10 | from pathlib import Path
11 | 
12 | from .simple_mcp_server import main
13 | from .config import get_settings, initialize_logging
14 | 
15 | def setup_logging():
16 |     """Set up logging configuration."""
17 |     try:
18 |         settings = get_settings()
19 |         initialize_logging(settings)
20 |     except Exception as e:
21 |         # Fallback logging if config fails
22 |         logging.basicConfig(
23 |             level=logging.INFO,
24 |             format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25 |         )
26 |         logging.getLogger(__name__).warning(f"Failed to load settings: {e}")
27 | 
28 | def check_env_file():
29 |     """Check if .env file exists and provide helpful guidance."""
30 |     env_path = Path(".env")
31 |     if not env_path.exists():
32 |         print("⚠️  No .env file found!")
33 |         print("Please create a .env file with your Jira configuration:")
34 |         print("")
35 |         print("JIRA_URL=https://your-domain.atlassian.net")
36 |         print("[email protected]")
37 |         print("JIRA_API_TOKEN=your_api_token")
38 |         print("PROJECT_KEY=PROJ")
39 |         print("DEFAULT_BOARD_ID=123")
40 |         print("")
41 |         print("You can copy .env.example to .env and edit it with your values.")
42 |         return False
43 |     return True
44 | 
45 | if __name__ == "__main__":
46 |     print("🚀 Starting MCP Jira Server...")
47 |     
48 |     setup_logging()
49 |     logger = logging.getLogger(__name__)
50 |     
51 |     if not check_env_file():
52 |         sys.exit(1)
53 |     
54 |     try:
55 |         logger.info("Initializing MCP Jira server...")
56 |         asyncio.run(main())
57 |     except KeyboardInterrupt:
58 |         logger.info("Server stopped by user")
59 |     except Exception as e:
60 |         logger.exception(f"Server failed to start: {e}")
61 |         print(f"❌ Error: {e}")
62 |         sys.exit(1) 
```

--------------------------------------------------------------------------------
/QUICKSTART.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Quick Start Guide
  2 | 
  3 | Get your MCP Jira server running in 5 minutes!
  4 | 
  5 | ## Prerequisites
  6 | 
  7 | - Python 3.8+
  8 | - Jira account with API access
  9 | - Claude Desktop or another MCP client
 10 | 
 11 | ## Step 1: Install
 12 | 
 13 | ```bash
 14 | cd mcp-jira
 15 | pip install -e .
 16 | ```
 17 | 
 18 | ## Step 2: Configure
 19 | 
 20 | Create a `.env` file:
 21 | 
 22 | ```bash
 23 | # Copy the example
 24 | cp .env.example .env
 25 | 
 26 | # Edit with your details
 27 | nano .env
 28 | ```
 29 | 
 30 | Required settings:
 31 | ```env
 32 | JIRA_URL=https://your-company.atlassian.net
 33 | [email protected]
 34 | JIRA_API_TOKEN=your_api_token_here
 35 | PROJECT_KEY=PROJ
 36 | ```
 37 | 
 38 | ## Step 3: Get Jira API Token
 39 | 
 40 | 1. Go to https://id.atlassian.com/manage-profile/security/api-tokens
 41 | 2. Click "Create API token"
 42 | 3. Copy the token to your `.env` file
 43 | 
 44 | ## Step 4: Test the Server
 45 | 
 46 | ```bash
 47 | python -m mcp_jira
 48 | ```
 49 | 
 50 | You should see: `🚀 Starting MCP Jira Server...`
 51 | 
 52 | ## Step 5: Connect to Claude Desktop
 53 | 
 54 | Add to your `claude_desktop_config.json`:
 55 | 
 56 | ```json
 57 | {
 58 |   "mcpServers": {
 59 |     "mcp-jira": {
 60 |       "command": "python",
 61 |       "args": ["-m", "mcp_jira"],
 62 |       "cwd": "/path/to/mcp-jira"
 63 |     }
 64 |   }
 65 | }
 66 | ```
 67 | 
 68 | ## Step 6: Try It Out!
 69 | 
 70 | In Claude Desktop, try:
 71 | 
 72 | - "Create a high priority bug for login issues"
 73 | - "What's our current sprint status?"
 74 | - "Show me all issues assigned to john.doe"
 75 | - "Generate today's standup report"
 76 | 
 77 | ## Troubleshooting
 78 | 
 79 | ### "No .env file found"
 80 | - Make sure you created `.env` in the project root
 81 | - Copy from `.env.example` if available
 82 | 
 83 | ### "Authentication failed"
 84 | - Check your API token is correct
 85 | - Verify your username is your email address
 86 | - Ensure the Jira URL is correct
 87 | 
 88 | ### "No active sprint found"
 89 | - Make sure your board has an active sprint
 90 | - Set `DEFAULT_BOARD_ID` in your `.env`
 91 | 
 92 | ### "Permission denied"
 93 | - Verify your Jira user has project access
 94 | - Check the `PROJECT_KEY` is correct
 95 | 
 96 | ## Next Steps
 97 | 
 98 | - Explore all available tools with "What tools do you have?"
 99 | - Set up team workload monitoring
100 | - Automate your daily standups
101 | - Create custom JQL searches
102 | 
103 | Happy project managing! 🎯 
```

--------------------------------------------------------------------------------
/src/mcp_jira/config.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Configuration management for MCP Jira.
  3 | Handles environment variables, settings validation, and configuration defaults.
  4 | """
  5 | 
  6 | from pydantic import HttpUrl, SecretStr, field_validator
  7 | from pydantic_settings import BaseSettings
  8 | from typing import Optional
  9 | import os
 10 | from functools import lru_cache
 11 | 
 12 | class Settings(BaseSettings):
 13 |     """
 14 |     Configuration settings for the MCP Jira application.
 15 |     Uses Pydantic for validation and environment variable loading.
 16 |     """
 17 |     # Jira Configuration
 18 |     jira_url: HttpUrl
 19 |     jira_username: str
 20 |     jira_api_token: SecretStr
 21 |     project_key: str
 22 |     default_board_id: Optional[int] = None
 23 | 
 24 |     # Application Settings
 25 |     debug_mode: bool = False
 26 |     log_level: str = "INFO"
 27 |     
 28 |     # Sprint Defaults
 29 |     default_sprint_length: int = 14  # days
 30 |     story_points_field: str = "customfield_10026"  # Default story points field
 31 |     max_sprint_items: int = 50
 32 |     
 33 |     # Performance Settings
 34 |     jira_request_timeout: int = 30  # seconds
 35 |     cache_ttl: int = 300  # seconds
 36 |     max_concurrent_requests: int = 10
 37 |     
 38 |     class Config:
 39 |         """Pydantic configuration"""
 40 |         env_file = ".env"
 41 |         env_file_encoding = "utf-8"
 42 |         case_sensitive = False
 43 | 
 44 |     @field_validator("log_level")
 45 |     @classmethod
 46 |     def validate_log_level(cls, v: str) -> str:
 47 |         """Validate log level is a valid Python logging level"""
 48 |         valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
 49 |         upper_v = v.upper()
 50 |         if upper_v not in valid_levels:
 51 |             raise ValueError(f"Log level must be one of {valid_levels}")
 52 |         return upper_v
 53 | 
 54 |     @field_validator("jira_url")
 55 |     @classmethod
 56 |     def validate_jira_url(cls, v: HttpUrl) -> HttpUrl:
 57 |         """Ensure Jira URL is properly formatted"""
 58 |         url_str = str(v)
 59 |         if not url_str.endswith("/"):
 60 |             url_str += "/"
 61 |         return HttpUrl(url_str)
 62 | 
 63 | @lru_cache()
 64 | def get_settings() -> Settings:
 65 |     """
 66 |     Get settings with LRU cache to avoid reading environment variables multiple times.
 67 |     """
 68 |     return Settings()
 69 | 
 70 | def initialize_logging(settings: Settings) -> None:
 71 |     """Initialize logging configuration"""
 72 |     import logging
 73 |     
 74 |     logging.basicConfig(
 75 |         level=settings.log_level,
 76 |         format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
 77 |     )
 78 | 
 79 |     # Set third-party loggers to WARNING to reduce noise
 80 |     logging.getLogger("aiohttp").setLevel(logging.WARNING)
 81 |     logging.getLogger("urllib3").setLevel(logging.WARNING)
 82 | 
 83 | # Example .env file template
 84 | ENV_TEMPLATE = """
 85 | # Jira Configuration
 86 | JIRA_URL=https://your-domain.atlassian.net
 87 | [email protected]
 88 | JIRA_API_TOKEN=your_api_token
 89 | PROJECT_KEY=PROJ
 90 | DEFAULT_BOARD_ID=123
 91 | 
 92 | # Application Settings
 93 | DEBUG_MODE=false
 94 | LOG_LEVEL=INFO
 95 | """
 96 | 
 97 | def generate_env_template() -> str:
 98 |     """Generate a template .env file"""
 99 |     return ENV_TEMPLATE.strip()
100 | 
```

--------------------------------------------------------------------------------
/tests/test_jira_client.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Tests for the Jira client implementation.
 3 | """
 4 | 
 5 | import pytest
 6 | from unittest.mock import Mock, AsyncMock, patch, MagicMock
 7 | from mcp_jira.jira_client import JiraClient
 8 | from mcp_jira.types import IssueType, Priority
 9 | 
10 | @pytest.mark.asyncio
11 | @patch('aiohttp.ClientSession')
12 | async def test_create_issue(mock_session_class, mock_jira_client):
13 |     """Test creating a Jira issue"""
14 |     # Set up mock session
15 |     mock_session = MagicMock()
16 |     mock_session_class.return_value.__aenter__.return_value = mock_session
17 |     mock_session_class.return_value.__aexit__.return_value = None
18 | 
19 |     # Mock the POST response
20 |     mock_response = MagicMock()
21 |     mock_response.status = 201
22 |     mock_response.json = AsyncMock(return_value={"key": "TEST-1"})
23 |     mock_session.post.return_value.__aenter__.return_value = mock_response
24 |     mock_session.post.return_value.__aexit__.return_value = None
25 | 
26 |     result = await mock_jira_client.create_issue(
27 |         summary="Test Issue",
28 |         description="Test Description",
29 |         issue_type=IssueType.STORY,
30 |         priority=Priority.HIGH,
31 |         story_points=5
32 |     )
33 |     assert result == "TEST-1"
34 | 
35 | @pytest.mark.asyncio
36 | async def test_get_sprint(mock_jira_client, sample_sprint):
37 |     """Test getting sprint details"""
38 |     sprint = await mock_jira_client.get_sprint(1)
39 |     assert sprint.id == 1
40 |     assert sprint.name == "Test Sprint"
41 | 
42 | @pytest.mark.asyncio
43 | async def test_get_sprint_issues(mock_jira_client, sample_issue):
44 |     """Test getting sprint issues"""
45 |     issues = await mock_jira_client.get_sprint_issues(1)
46 |     assert len(issues) > 0
47 |     assert issues[0].key == sample_issue.key
48 |     assert issues[0].summary == sample_issue.summary
49 | 
50 | @pytest.mark.asyncio
51 | async def test_get_backlog_issues(mock_jira_client):
52 |     """Test getting backlog issues"""
53 |     issues = await mock_jira_client.get_backlog_issues()
54 |     assert len(issues) > 0
55 |     assert all(isinstance(issue.key, str) for issue in issues)
56 | 
57 | @pytest.mark.asyncio
58 | async def test_search_issues(mock_jira_client):
59 |     """Test searching issues"""
60 |     jql = 'project = "TEST"'
61 |     issues = await mock_jira_client.search_issues(jql)
62 |     assert len(issues) > 0
63 |     assert all(hasattr(issue, 'key') for issue in issues)
64 | 
65 | @pytest.mark.asyncio
66 | async def test_get_issue_history(mock_jira_client):
67 |     """Test getting issue history"""
68 |     history = await mock_jira_client.get_issue_history("TEST-1")
69 |     assert isinstance(history, list)
70 | 
71 | @pytest.mark.asyncio
72 | async def test_get_assigned_issues(mock_jira_client):
73 |     """Test getting assigned issues"""
74 |     issues = await mock_jira_client.get_assigned_issues("test_user")
75 |     assert len(issues) > 0
76 |     assert all(hasattr(issue, 'assignee') for issue in issues)
77 | 
78 | @pytest.mark.asyncio
79 | async def test_error_handling(mock_jira_client, mock_response):
80 |     """Test error handling"""
81 |     # Mock error response
82 |     mock_jira_client._session.post = mock_response(500, {"error": "Test error"})
83 |     
84 |     with pytest.raises(Exception):
85 |         await mock_jira_client.create_issue(
86 |             summary="Test Issue",
87 |             description="Test Description",
88 |             issue_type=IssueType.STORY,
89 |             priority=Priority.HIGH
90 |         )
```

--------------------------------------------------------------------------------
/src/mcp_jira/types.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Type definitions and enums for the MCP Jira server.
  3 | Includes all custom types used across the application.
  4 | """
  5 | 
  6 | from enum import Enum, auto
  7 | from typing import List, Dict, Any, Optional
  8 | from pydantic import BaseModel, Field
  9 | from datetime import datetime
 10 | 
 11 | class IssueType(str, Enum):
 12 |     """Jira issue types"""
 13 |     STORY = "Story"
 14 |     BUG = "Bug"
 15 |     TASK = "Task"
 16 |     EPIC = "Epic"
 17 |     SUBTASK = "Sub-task"
 18 |     INCIDENT = "Incident"
 19 |     SERVICE_REQUEST = "Service Request"
 20 | 
 21 | class Priority(str, Enum):
 22 |     """Jira priority levels"""
 23 |     HIGHEST = "Highest"
 24 |     HIGH = "High"
 25 |     MEDIUM = "Medium"
 26 |     LOW = "Low"
 27 |     LOWEST = "Lowest"
 28 | 
 29 | class SprintStatus(str, Enum):
 30 |     """Sprint statuses"""
 31 |     PLANNING = "Planning"
 32 |     ACTIVE = "Active"
 33 |     COMPLETED = "Completed"
 34 |     CANCELLED = "Cancelled"
 35 | 
 36 | class IssueStatus(str, Enum):
 37 |     """Issue statuses"""
 38 |     TODO = "To Do"
 39 |     IN_PROGRESS = "In Progress"
 40 |     REVIEW = "Review"
 41 |     BLOCKED = "Blocked"
 42 |     DONE = "Done"
 43 | 
 44 | class RiskLevel(str, Enum):
 45 |     """Risk levels for sprint analysis"""
 46 |     HIGH = "High"
 47 |     MEDIUM = "Medium"
 48 |     LOW = "Low"
 49 | 
 50 | class RiskType(str, Enum):
 51 |     """Types of risks that can be identified"""
 52 |     SCOPE_CREEP = "Scope Creep"
 53 |     RESOURCE_CONSTRAINT = "Resource Constraint"
 54 |     TECHNICAL_DEBT = "Technical Debt"
 55 |     DEPENDENCY_RISK = "Dependency Risk"
 56 |     VELOCITY_RISK = "Velocity Risk"
 57 |     CAPACITY_RISK = "Capacity Risk"
 58 | 
 59 | # Pydantic models for structured data
 60 | class TeamMember(BaseModel):
 61 |     """Team member information"""
 62 |     username: str
 63 |     display_name: str
 64 |     email: Optional[str]
 65 |     role: Optional[str]
 66 |     capacity: Optional[float] = Field(
 67 |         default=1.0,
 68 |         description="Capacity as percentage (1.0 = 100%)"
 69 |     )
 70 | 
 71 | class Issue(BaseModel):
 72 |     """Jira issue details"""
 73 |     key: str
 74 |     summary: str
 75 |     description: Optional[str]
 76 |     issue_type: IssueType
 77 |     priority: Priority
 78 |     status: IssueStatus
 79 |     assignee: Optional[TeamMember]
 80 |     story_points: Optional[float]
 81 |     labels: List[str] = []
 82 |     components: List[str] = []
 83 |     created_at: datetime
 84 |     updated_at: datetime
 85 |     blocked_by: List[str] = []
 86 |     blocks: List[str] = []
 87 | 
 88 | class Sprint(BaseModel):
 89 |     """Sprint information"""
 90 |     id: int
 91 |     name: str
 92 |     goal: Optional[str]
 93 |     status: SprintStatus
 94 |     start_date: Optional[datetime]
 95 |     end_date: Optional[datetime]
 96 |     completed_points: float = 0
 97 |     total_points: float = 0
 98 |     team_members: List[TeamMember] = []
 99 | 
100 | class Risk(BaseModel):
101 |     """Risk assessment details"""
102 |     type: RiskType
103 |     level: RiskLevel
104 |     description: str
105 |     impact: str
106 |     mitigation: Optional[str]
107 |     affected_issues: List[str] = []
108 | 
109 | class SprintMetrics(BaseModel):
110 |     """Sprint performance metrics"""
111 |     velocity: float
112 |     completion_rate: float
113 |     average_cycle_time: float
114 |     blocked_issues_count: int
115 |     scope_changes: int
116 |     team_capacity: float
117 |     burndown_ideal: Dict[str, float]
118 |     burndown_actual: Dict[str, float]
119 | 
120 | class WorkloadBalance(BaseModel):
121 |     """Workload distribution information"""
122 |     team_member: TeamMember
123 |     assigned_points: float
124 |     issue_count: int
125 |     current_capacity: float
126 |     recommendations: List[str]
127 | 
128 | class DailyStandupItem(BaseModel):
129 |     """Individual standup update"""
130 |     issue_key: str
131 |     summary: str
132 |     status: IssueStatus
133 |     assignee: str
134 |     blocked_reason: Optional[str]
135 |     notes: Optional[str]
136 |     time_spent: Optional[float]
137 | 
138 | # Custom exceptions
139 | class JiraError(Exception):
140 |     """Base exception for Jira-related errors"""
141 |     pass
142 | 
143 | class SprintError(Exception):
144 |     """Base exception for Sprint-related errors"""
145 |     pass
146 | 
147 | class ConfigError(Exception):
148 |     """Base exception for configuration errors"""
149 |     pass
150 | 
151 | # Type aliases for complex types
152 | SprintPlanningResult = Dict[str, List[Issue]]
153 | WorkloadDistribution = Dict[str, WorkloadBalance]
154 | RiskAssessment = List[Risk]
155 | TeamCapacityMap = Dict[str, float]
156 | 
```

--------------------------------------------------------------------------------
/tests/test_simple_mcp_server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the simple MCP server implementation.
  3 | """
  4 | 
  5 | import pytest
  6 | import asyncio
  7 | from unittest.mock import Mock, AsyncMock, patch
  8 | 
  9 | from mcp_jira.simple_mcp_server import (
 10 |     list_tools, call_tool, handle_create_issue, 
 11 |     handle_search_issues, handle_sprint_status
 12 | )
 13 | from mcp_jira.types import IssueType, Priority, Issue, Sprint, IssueStatus, SprintStatus
 14 | from mcp.types import Tool, TextContent
 15 | 
 16 | @pytest.mark.asyncio
 17 | async def test_list_tools():
 18 |     """Test that tools are properly listed"""
 19 |     tools = await list_tools()
 20 |     
 21 |     assert len(tools) == 5
 22 |     tool_names = [tool.name for tool in tools]
 23 |     
 24 |     expected_tools = [
 25 |         "create_issue", "search_issues", "get_sprint_status", 
 26 |         "get_team_workload", "generate_standup_report"
 27 |     ]
 28 |     
 29 |     for expected_tool in expected_tools:
 30 |         assert expected_tool in tool_names
 31 | 
 32 | @pytest.mark.asyncio
 33 | async def test_create_issue_tool():
 34 |     """Test create_issue tool"""
 35 |     with patch('mcp_jira.simple_mcp_server.jira_client') as mock_client:
 36 |         mock_client.create_issue = AsyncMock(return_value="TEST-123")
 37 |         
 38 |         args = {
 39 |             "summary": "Test Issue",
 40 |             "description": "Test Description", 
 41 |             "issue_type": "Story",
 42 |             "priority": "High"
 43 |         }
 44 |         
 45 |         result = await handle_create_issue(args)
 46 |         
 47 |         assert len(result) == 1
 48 |         assert isinstance(result[0], TextContent)
 49 |         assert "TEST-123" in result[0].text
 50 |         assert "✅" in result[0].text
 51 | 
 52 | @pytest.mark.asyncio
 53 | async def test_search_issues_tool():
 54 |     """Test search_issues tool"""
 55 |     with patch('mcp_jira.simple_mcp_server.jira_client') as mock_client:
 56 |         # Mock issue data
 57 |         mock_issue = Mock()
 58 |         mock_issue.key = "TEST-1"
 59 |         mock_issue.summary = "Test Issue"
 60 |         mock_issue.status.value = "In Progress"
 61 |         mock_issue.priority.value = "High"
 62 |         mock_issue.assignee = None
 63 |         mock_issue.story_points = 5
 64 |         
 65 |         mock_client.search_issues = AsyncMock(return_value=[mock_issue])
 66 |         
 67 |         args = {"jql": "project = TEST"}
 68 |         result = await handle_search_issues(args)
 69 |         
 70 |         assert len(result) == 1
 71 |         assert isinstance(result[0], TextContent)
 72 |         assert "TEST-1" in result[0].text
 73 |         assert "Test Issue" in result[0].text
 74 | 
 75 | @pytest.mark.asyncio
 76 | async def test_search_issues_no_results():
 77 |     """Test search_issues with no results"""
 78 |     with patch('mcp_jira.simple_mcp_server.jira_client') as mock_client:
 79 |         mock_client.search_issues = AsyncMock(return_value=[])
 80 |         
 81 |         args = {"jql": "project = EMPTY"}
 82 |         result = await handle_search_issues(args)
 83 |         
 84 |         assert len(result) == 1
 85 |         assert "No issues found" in result[0].text
 86 | 
 87 | @pytest.mark.asyncio
 88 | async def test_sprint_status_tool():
 89 |     """Test get_sprint_status tool"""
 90 |     with patch('mcp_jira.simple_mcp_server.jira_client') as mock_client:
 91 |         # Mock sprint data
 92 |         mock_sprint = Mock()
 93 |         mock_sprint.id = 1
 94 |         mock_sprint.name = "Test Sprint"
 95 |         mock_sprint.status.value = "Active"
 96 |         mock_sprint.goal = "Complete features"
 97 |         mock_sprint.start_date = None
 98 |         mock_sprint.end_date = None
 99 |         
100 |         # Mock issues
101 |         mock_issue = Mock()
102 |         mock_issue.story_points = 5
103 |         mock_issue.status.value = "Done"
104 |         
105 |         mock_client.get_active_sprint = AsyncMock(return_value=mock_sprint)
106 |         mock_client.get_sprint_issues = AsyncMock(return_value=[mock_issue])
107 |         
108 |         args = {}
109 |         result = await handle_sprint_status(args)
110 |         
111 |         assert len(result) == 1
112 |         assert isinstance(result[0], TextContent)
113 |         assert "Test Sprint" in result[0].text
114 |         assert "📊" in result[0].text
115 | 
116 | @pytest.mark.asyncio
117 | async def test_call_tool_unknown():
118 |     """Test calling an unknown tool"""
119 |     with patch('mcp_jira.simple_mcp_server.jira_client', Mock()):
120 |         result = await call_tool("unknown_tool", {})
121 |         
122 |         assert len(result) == 1
123 |         assert "Unknown tool" in result[0].text
124 | 
125 | @pytest.mark.asyncio
126 | async def test_call_tool_no_client():
127 |     """Test calling tool when client is not initialized"""
128 |     with patch('mcp_jira.simple_mcp_server.jira_client', None):
129 |         result = await call_tool("create_issue", {})
130 |         
131 |         assert len(result) == 1
132 |         assert "Jira client not initialized" in result[0].text 
```

--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | PyTest configuration and fixtures for MCP Jira tests.
  3 | """
  4 | 
  5 | import pytest
  6 | from typing import Dict, Any
  7 | import aiohttp
  8 | from datetime import datetime
  9 | from unittest.mock import MagicMock, AsyncMock
 10 | 
 11 | from mcp_jira.config import Settings
 12 | from mcp_jira.jira_client import JiraClient
 13 | from mcp_jira.types import Issue, Sprint, TeamMember, IssueType, Priority, IssueStatus
 14 | 
 15 | @pytest.fixture
 16 | def test_settings():
 17 |     """Provide test settings"""
 18 |     # Mock environment variables for testing
 19 |     import os
 20 |     os.environ["JIRA_URL"] = "https://test-jira.example.com"
 21 |     os.environ["JIRA_USERNAME"] = "test_user"
 22 |     os.environ["JIRA_API_TOKEN"] = "test_token"
 23 |     os.environ["PROJECT_KEY"] = "TEST"
 24 |     os.environ["DEFAULT_BOARD_ID"] = "1"
 25 | 
 26 |     return Settings()
 27 | 
 28 | @pytest.fixture
 29 | def mock_response():
 30 |     """Create a mock aiohttp response"""
 31 |     class MockResponse:
 32 |         def __init__(self, status: int, data: Dict[str, Any]):
 33 |             self.status = status
 34 |             self._data = data
 35 | 
 36 |         async def json(self):
 37 |             return self._data
 38 | 
 39 |         async def text(self):
 40 |             return str(self._data)
 41 | 
 42 |         async def __aenter__(self):
 43 |             return self
 44 | 
 45 |         async def __aexit__(self, exc_type, exc_val, exc_tb):
 46 |             pass
 47 | 
 48 |     return MockResponse
 49 | 
 50 | @pytest.fixture
 51 | def mock_jira_client(test_settings):
 52 |     """Create a mock Jira client"""
 53 |     client = JiraClient(test_settings)
 54 | 
 55 |     # Mock the entire session to prevent HTTP calls
 56 |     client._session = MagicMock()
 57 | 
 58 |     # Mock all HTTP methods to return successful responses
 59 |     async def mock_get(*args, **kwargs):
 60 |         # Mock sprint response
 61 |         if "sprint" in str(args[0]):
 62 |             return MagicMock(status=200, json=AsyncMock(return_value={
 63 |                 "id": 1,
 64 |                 "name": "Test Sprint",
 65 |                 "goal": "Test Goal",
 66 |                 "state": "active",
 67 |                 "startDate": "2024-01-08T00:00:00.000Z",
 68 |                 "endDate": "2024-01-22T00:00:00.000Z"
 69 |             }))
 70 |         # Mock issue response
 71 |         elif "issue" in str(args[0]):
 72 |             return MagicMock(status=200, json=AsyncMock(return_value={
 73 |                 "issues": [{
 74 |                     "key": "TEST-1",
 75 |                     "fields": {
 76 |                         "summary": "Test Issue",
 77 |                         "description": "Test Description",
 78 |                         "issuetype": {"name": "Story"},
 79 |                         "priority": {"name": "High"},
 80 |                         "status": {"name": "To Do"},
 81 |                         "assignee": {
 82 |                             "name": "test_user",
 83 |                             "displayName": "Test User",
 84 |                             "emailAddress": "[email protected]"
 85 |                         },
 86 |                         "created": "2024-01-08T10:00:00.000Z",
 87 |                         "updated": "2024-01-08T10:00:00.000Z",
 88 |                         "customfield_10026": 5
 89 |                     }
 90 |                 }]
 91 |             }))
 92 | 
 93 |     async def mock_post(*args, **kwargs):
 94 |         # Mock issue creation
 95 |         if "issue" in str(args[0]):
 96 |             return MagicMock(status=201, json=AsyncMock(return_value={"key": "TEST-1"}))
 97 |         # Mock search
 98 |         else:
 99 |             return MagicMock(status=200, json=AsyncMock(return_value={
100 |                 "issues": [{
101 |                     "key": "TEST-1",
102 |                     "fields": {
103 |                         "summary": "Test Issue",
104 |                         "description": "Test Description",
105 |                         "issuetype": {"name": "Story"},
106 |                         "priority": {"name": "High"},
107 |                         "status": {"name": "To Do"},
108 |                         "assignee": {
109 |                             "name": "test_user",
110 |                             "displayName": "Test User",
111 |                             "emailAddress": "[email protected]"
112 |                         },
113 |                         "created": "2024-01-08T10:00:00.000Z",
114 |                         "updated": "2024-01-08T10:00:00.000Z",
115 |                         "customfield_10026": 5
116 |                     }
117 |                 }]
118 |             }))
119 | 
120 |     client._session.get = AsyncMock(side_effect=mock_get)
121 |     client._session.post = AsyncMock(side_effect=mock_post)
122 | 
123 |     return client
124 | 
125 | @pytest.fixture
126 | def sample_issue():
127 |     """Provide a sample issue"""
128 |     return Issue(
129 |             key="TEST-1",
130 |             summary="Test Issue",
131 |             description="Test Description",
132 |             issue_type=IssueType.STORY,
133 |             priority=Priority.HIGH,
134 |             status=IssueStatus.TODO,
135 |             assignee=TeamMember(
136 |                 username="test_user",
137 |                 display_name="Test User",
138 |                 email="[email protected]",
139 |                 role="Developer"
140 |             ),
141 |             story_points=5,
142 |             labels=[],
143 |             components=[],
144 |             created_at=datetime.fromisoformat("2024-01-08T10:00:00.000"),
145 |             updated_at=datetime.fromisoformat("2024-01-08T10:00:00.000"),
146 |             blocked_by=[],
147 |             blocks=[]
148 |         )
149 | 
150 | @pytest.fixture
151 | def sample_sprint():
152 |     """Provide a sample sprint"""
153 |     return {
154 |         "id": 1,
155 |         "name": "Test Sprint",
156 |         "goal": "Test Goal",
157 |         "state": "active",
158 |         "startDate": "2024-01-08T00:00:00.000Z",
159 |         "endDate": "2024-01-22T00:00:00.000Z"
160 |     }
```

--------------------------------------------------------------------------------
/src/mcp_jira/mcp_protocol.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | MCP (Model Context Protocol) implementation for Jira integration.
  3 | Handles function definitions, resource management, and protocol handlers.
  4 | """
  5 | 
  6 | from enum import Enum
  7 | from typing import Dict, Any, List, Optional, Union, Callable
  8 | from pydantic import BaseModel, Field
  9 | import asyncio
 10 | import logging
 11 | from datetime import datetime, timezone
 12 | 
 13 | from .types import (
 14 |     Issue, Sprint, TeamMember, SprintStatus,
 15 |     IssueType, Priority, Risk
 16 | )
 17 | from .jira_client import JiraClient
 18 | 
 19 | logger = logging.getLogger(__name__)
 20 | 
 21 | class MCPResourceType(str, Enum):
 22 |     """MCP Resource Types"""
 23 |     ISSUE = "issue"
 24 |     SPRINT = "sprint"
 25 |     TEAM = "team"
 26 |     METRICS = "metrics"
 27 |     REPORT = "report"
 28 | 
 29 | class MCPFunction(BaseModel):
 30 |     """MCP Function Definition"""
 31 |     name: str
 32 |     description: str
 33 |     resource_type: MCPResourceType
 34 |     parameters: Dict[str, Any]
 35 |     returns: Dict[str, Any]
 36 |     handler: Optional[str] = None
 37 | 
 38 | class MCPContext(BaseModel):
 39 |     """MCP Context Information"""
 40 |     conversation_id: str
 41 |     user_id: str
 42 |     timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
 43 |     metadata: Dict[str, Any] = Field(default_factory=dict)
 44 | 
 45 | class MCPRequest(BaseModel):
 46 |     """MCP Request Structure"""
 47 |     function: str
 48 |     parameters: Dict[str, Any]
 49 |     context: MCPContext
 50 |     resource_type: MCPResourceType
 51 | 
 52 | class MCPResponse(BaseModel):
 53 |     """MCP Response Structure"""
 54 |     status: str
 55 |     data: Optional[Dict[str, Any]] = None
 56 |     error: Optional[str] = None
 57 |     context: MCPContext
 58 | 
 59 | class MCPProtocolHandler:
 60 |     """
 61 |     Main handler for MCP protocol implementation.
 62 |     Manages resources, functions, and request processing.
 63 |     """
 64 |     def __init__(self, jira_client: JiraClient):
 65 |         self.jira = jira_client
 66 |         self.functions: Dict[str, MCPFunction] = {}
 67 |         self._register_core_functions()
 68 | 
 69 |     def _register_core_functions(self):
 70 |         """Register core MCP functions"""
 71 |         self.register_function(
 72 |             MCPFunction(
 73 |                 name="create_issue",
 74 |                 description="Create a new Jira issue",
 75 |                 resource_type=MCPResourceType.ISSUE,
 76 |                 parameters={
 77 |                     "summary": {"type": "string", "required": True},
 78 |                     "description": {"type": "string", "required": True},
 79 |                     "issue_type": {"type": "string", "enum": [t.value for t in IssueType]},
 80 |                     "priority": {"type": "string", "enum": [p.value for p in Priority]},
 81 |                     "story_points": {"type": "number", "required": False},
 82 |                     "assignee": {"type": "string", "required": False}
 83 |                 },
 84 |                 returns={
 85 |                     "issue_key": {"type": "string"}
 86 |                 },
 87 |                 handler="handle_create_issue"
 88 |             )
 89 |         )
 90 | 
 91 |     def register_function(self, function: MCPFunction):
 92 |         """Register a new MCP function"""
 93 |         self.functions[function.name] = function
 94 |         logger.info(f"Registered MCP function: {function.name}")
 95 | 
 96 |     async def process_request(self, request: MCPRequest) -> MCPResponse:
 97 |         """Process an MCP request"""
 98 |         try:
 99 |             if request.function not in self.functions:
100 |                 return MCPResponse(
101 |                     status="error",
102 |                     error=f"Unknown function: {request.function}",
103 |                     context=request.context
104 |                 )
105 | 
106 |             function = self.functions[request.function]
107 |             if function.resource_type != request.resource_type:
108 |                 return MCPResponse(
109 |                     status="error",
110 |                     error=f"Invalid resource type for function {request.function}",
111 |                     context=request.context
112 |                 )
113 | 
114 |             handler = getattr(self, function.handler)
115 |             if not handler:
116 |                 return MCPResponse(
117 |                     status="error",
118 |                     error=f"Handler not implemented: {function.handler}",
119 |                     context=request.context
120 |                 )
121 | 
122 |             result = await handler(request.parameters, request.context)
123 |             
124 |             return MCPResponse(
125 |                 status="success",
126 |                 data=result,
127 |                 context=request.context
128 |             )
129 | 
130 |         except Exception as e:
131 |             logger.exception(f"Error processing MCP request: {str(e)}")
132 |             return MCPResponse(
133 |                 status="error",
134 |                 error=str(e),
135 |                 context=request.context
136 |             )
137 | 
138 |     # Handler implementations
139 |     async def handle_create_issue(
140 |         self, 
141 |         parameters: Dict[str, Any], 
142 |         context: MCPContext
143 |     ) -> Dict[str, Any]:
144 |         """Handle create_issue function"""
145 |         issue_key = await self.jira.create_issue(
146 |             summary=parameters["summary"],
147 |             description=parameters["description"],
148 |             issue_type=IssueType(parameters["issue_type"]),
149 |             priority=Priority(parameters["priority"]),
150 |             story_points=parameters.get("story_points"),
151 |             assignee=parameters.get("assignee")
152 |         )
153 |         return {"issue_key": issue_key}
154 | 
155 |     # Resource handlers
156 |     async def get_resource(
157 |         self, 
158 |         resource_type: MCPResourceType, 
159 |         resource_id: str
160 |     ) -> Dict[str, Any]:
161 |         """Get a resource by type and ID"""
162 |         handlers = {
163 |             MCPResourceType.ISSUE: self.jira.get_issue,
164 |             MCPResourceType.SPRINT: self.jira.get_sprint,
165 |             # Add more resource handlers...
166 |         }
167 |         
168 |         handler = handlers.get(resource_type)
169 |         if not handler:
170 |             raise ValueError(f"Unknown resource type: {resource_type}")
171 |             
172 |         return await handler(resource_id)
173 | 
174 |     async def update_resource(
175 |         self, 
176 |         resource_type: MCPResourceType, 
177 |         resource_id: str, 
178 |         updates: Dict[str, Any]
179 |     ) -> Dict[str, Any]:
180 |         """Update a resource"""
181 |         # Implement resource update logic
182 |         pass
183 | 
```

--------------------------------------------------------------------------------
/src/mcp_jira/jira_client.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | JiraClient class implementation for MCP Jira.
  3 | Handles all direct interactions with the Jira API.
  4 | """
  5 | 
  6 | from typing import List, Optional, Dict, Any
  7 | import aiohttp
  8 | import logging
  9 | from datetime import datetime
 10 | from base64 import b64encode
 11 | 
 12 | from .types import (
 13 |     Issue, Sprint, TeamMember, IssueType, 
 14 |     Priority, IssueStatus, SprintStatus,
 15 |     JiraError
 16 | )
 17 | from .config import Settings
 18 | 
 19 | logger = logging.getLogger(__name__)
 20 | 
 21 | class JiraClient:
 22 |     def __init__(self, settings: Settings):
 23 |         self.base_url = str(settings.jira_url).rstrip('/')
 24 |         self.auth_header = self._create_auth_header(
 25 |             settings.jira_username,
 26 |             settings.jira_api_token
 27 |         )
 28 |         self.project_key = settings.project_key
 29 |         self.board_id = settings.default_board_id
 30 | 
 31 |     async def create_issue(
 32 |         self,
 33 |         summary: str,
 34 |         description: str,
 35 |         issue_type: IssueType,
 36 |         priority: Priority,
 37 |         story_points: Optional[float] = None,
 38 |         assignee: Optional[str] = None,
 39 |         labels: Optional[List[str]] = None,
 40 |         components: Optional[List[str]] = None
 41 |     ) -> str:
 42 |         """Create a new Jira issue."""
 43 |         data = {
 44 |             "fields": {
 45 |                 "project": {"key": self.project_key},
 46 |                 "summary": summary,
 47 |                 "description": description,
 48 |                 "issuetype": {"name": issue_type.value},
 49 |                 "priority": {"name": priority.value}
 50 |             }
 51 |         }
 52 | 
 53 |         if story_points:
 54 |             data["fields"]["customfield_10026"] = story_points  # Adjust field ID as needed
 55 |         if assignee:
 56 |             data["fields"]["assignee"] = {"name": assignee}
 57 |         if labels:
 58 |             data["fields"]["labels"] = labels
 59 |         if components:
 60 |             data["fields"]["components"] = [{"name": c} for c in components]
 61 | 
 62 |         async with aiohttp.ClientSession() as session:
 63 |             async with session.post(
 64 |                 f"{self.base_url}/rest/api/2/issue",
 65 |                 headers=self._get_headers(),
 66 |                 json=data
 67 |             ) as response:
 68 |                 if response.status == 201:
 69 |                     result = await response.json()
 70 |                     return result["key"]
 71 |                 else:
 72 |                     error_data = await response.text()
 73 |                     raise JiraError(f"Failed to create issue: {error_data}")
 74 | 
 75 |     async def get_sprint(self, sprint_id: int) -> Sprint:
 76 |         """Get sprint details by ID."""
 77 |         async with aiohttp.ClientSession() as session:
 78 |             async with session.get(
 79 |                 f"{self.base_url}/rest/agile/1.0/sprint/{sprint_id}",
 80 |                 headers=self._get_headers()
 81 |             ) as response:
 82 |                 if response.status == 200:
 83 |                     data = await response.json()
 84 |                     return self._convert_to_sprint(data)
 85 |                 else:
 86 |                     error_data = await response.text()
 87 |                     raise JiraError(f"Failed to get sprint: {error_data}")
 88 | 
 89 |     async def get_active_sprint(self) -> Optional[Sprint]:
 90 |         """Get the currently active sprint."""
 91 |         sprints = await self._get_board_sprints(
 92 |             self.board_id, 
 93 |             state=SprintStatus.ACTIVE
 94 |         )
 95 |         return sprints[0] if sprints else None
 96 | 
 97 |     async def get_sprint_issues(self, sprint_id: int) -> List[Issue]:
 98 |         """Get all issues in a sprint."""
 99 |         async with aiohttp.ClientSession() as session:
100 |             async with session.get(
101 |                 f"{self.base_url}/rest/agile/1.0/sprint/{sprint_id}/issue",
102 |                 headers=self._get_headers()
103 |             ) as response:
104 |                 if response.status == 200:
105 |                     data = await response.json()
106 |                     return [self._convert_to_issue(i) for i in data["issues"]]
107 |                 else:
108 |                     error_data = await response.text()
109 |                     raise JiraError(f"Failed to get sprint issues: {error_data}")
110 | 
111 |     async def get_backlog_issues(self) -> List[Issue]:
112 |         """Get all backlog issues."""
113 |         jql = f"project = {self.project_key} AND sprint is EMPTY ORDER BY Rank ASC"
114 |         return await self.search_issues(jql)
115 | 
116 |     async def get_assigned_issues(self, username: str) -> List[Issue]:
117 |         """Get issues assigned to a specific user."""
118 |         jql = f"assignee = {username} AND resolution = Unresolved"
119 |         return await self.search_issues(jql)
120 | 
121 |     async def search_issues(self, jql: str) -> List[Issue]:
122 |         """Search issues using JQL."""
123 |         async with aiohttp.ClientSession() as session:
124 |             async with session.post(
125 |                 f"{self.base_url}/rest/api/2/search",
126 |                 headers=self._get_headers(),
127 |                 json={
128 |                     "jql": jql,
129 |                     "maxResults": 100
130 |                 }
131 |             ) as response:
132 |                 if response.status == 200:
133 |                     data = await response.json()
134 |                     return [self._convert_to_issue(i) for i in data["issues"]]
135 |                 else:
136 |                     error_data = await response.text()
137 |                     raise JiraError(f"Failed to search issues: {error_data}")
138 | 
139 |     async def get_issue_history(self, issue_key: str) -> List[Dict[str, Any]]:
140 |         """Get the change history of an issue."""
141 |         async with aiohttp.ClientSession() as session:
142 |             async with session.get(
143 |                 f"{self.base_url}/rest/api/2/issue/{issue_key}/changelog",
144 |                 headers=self._get_headers()
145 |             ) as response:
146 |                 if response.status == 200:
147 |                     data = await response.json()
148 |                     return self._process_changelog(data["values"])
149 |                 else:
150 |                     error_data = await response.text()
151 |                     raise JiraError(f"Failed to get issue history: {error_data}")
152 | 
153 |     # Helper methods
154 |     def _get_headers(self) -> Dict[str, str]:
155 |         """Get headers for Jira API requests."""
156 |         return {
157 |             "Authorization": f"Basic {self.auth_header}",
158 |             "Content-Type": "application/json",
159 |             "Accept": "application/json"
160 |         }
161 | 
162 |     def _create_auth_header(self, username: str, api_token: str) -> str:
163 |         """Create base64 encoded auth header."""
164 |         auth_string = f"{username}:{api_token}"
165 |         return b64encode(auth_string.encode()).decode()
166 | 
167 |     def _convert_to_issue(self, data: Dict[str, Any]) -> Issue:
168 |         """Convert Jira API response to Issue object."""
169 |         fields = data["fields"]
170 |         return Issue(
171 |             key=data["key"],
172 |             summary=fields["summary"],
173 |             description=fields.get("description"),
174 |             issue_type=IssueType(fields["issuetype"]["name"]),
175 |             priority=Priority(fields["priority"]["name"]),
176 |             status=IssueStatus(fields["status"]["name"]),
177 |             assignee=self._convert_to_team_member(fields.get("assignee")) if fields.get("assignee") else None,
178 |             story_points=fields.get("customfield_10026"),  # Adjust field ID as needed
179 |             labels=fields.get("labels", []),
180 |             components=[c["name"] for c in fields.get("components", [])],
181 |             created_at=datetime.fromisoformat(fields["created"].rstrip('Z')),
182 |             updated_at=datetime.fromisoformat(fields["updated"].rstrip('Z')),
183 |             blocked_by=[],  # Would need to implement logic to determine blockers
184 |             blocks=[]
185 |         )
186 | 
187 |     def _convert_to_sprint(self, data: Dict[str, Any]) -> Sprint:
188 |         """Convert Jira API response to Sprint object."""
189 |         return Sprint(
190 |             id=data["id"],
191 |             name=data["name"],
192 |             goal=data.get("goal"),
193 |             status=SprintStatus(data["state"]),
194 |             start_date=datetime.fromisoformat(data["startDate"].rstrip('Z')) if data.get("startDate") else None,
195 |             end_date=datetime.fromisoformat(data["endDate"].rstrip('Z')) if data.get("endDate") else None
196 |         )
197 | 
198 |     def _convert_to_team_member(self, data: Dict[str, Any]) -> TeamMember:
199 |         """Convert Jira API response to TeamMember object."""
200 |         return TeamMember(
201 |             username=data["name"],
202 |             display_name=data["displayName"],
203 |             email=data.get("emailAddress")
204 |         )
205 | 
206 |     def _process_changelog(self, changelog: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
207 |         """Process issue changelog into a more usable format."""
208 |         history = []
209 |         for entry in changelog:
210 |             for item in entry["items"]:
211 |                 if item["field"] == "status":
212 |                     history.append({
213 |                         "from_status": item["fromString"],
214 |                         "to_status": item["toString"],
215 |                         "from_date": datetime.fromisoformat(entry["created"].rstrip('Z')),
216 |                         "author": entry["author"]["displayName"]
217 |                     })
218 |         return history
219 | 
220 |     async def _get_board_sprints(
221 |         self, 
222 |         board_id: int, 
223 |         state: Optional[SprintStatus] = None
224 |     ) -> List[Sprint]:
225 |         """Get all sprints for a board."""
226 |         params = {"state": state.value} if state else {}
227 |         async with aiohttp.ClientSession() as session:
228 |             async with session.get(
229 |                 f"{self.base_url}/rest/agile/1.0/board/{board_id}/sprint",
230 |                 headers=self._get_headers(),
231 |                 params=params
232 |             ) as response:
233 |                 if response.status == 200:
234 |                     data = await response.json()
235 |                     return [self._convert_to_sprint(s) for s in data["values"]]
236 |                 else:
237 |                     error_data = await response.text()
238 |                     raise JiraError(f"Failed to get board sprints: {error_data}")
239 | 
```

--------------------------------------------------------------------------------
/src/mcp_jira/simple_mcp_server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Simple MCP server for Jira integration.
  3 | Implements core project management functions following MCP specification.
  4 | """
  5 | 
  6 | import asyncio
  7 | import logging
  8 | from typing import List, Dict, Any, Optional
  9 | from datetime import datetime
 10 | 
 11 | from mcp.server import Server
 12 | from mcp.server.stdio import stdio_server
 13 | from mcp.types import (
 14 |     Tool, TextContent, ImageContent, EmbeddedResource
 15 | )
 16 | 
 17 | from .jira_client import JiraClient
 18 | from .config import get_settings
 19 | from .types import IssueType, Priority
 20 | 
 21 | logger = logging.getLogger(__name__)
 22 | 
 23 | # Initialize server
 24 | server = Server("mcp-jira")
 25 | 
 26 | # Global client (will be initialized in main)
 27 | jira_client: Optional[JiraClient] = None
 28 | 
 29 | @server.list_tools()
 30 | async def list_tools() -> List[Tool]:
 31 |     """List available MCP tools for Jira operations."""
 32 |     return [
 33 |         Tool(
 34 |             name="create_issue",
 35 |             description="Create a new Jira issue",
 36 |             inputSchema={
 37 |                 "type": "object",
 38 |                 "properties": {
 39 |                     "summary": {
 40 |                         "type": "string",
 41 |                         "description": "Brief summary of the issue"
 42 |                     },
 43 |                     "description": {
 44 |                         "type": "string", 
 45 |                         "description": "Detailed description of the issue"
 46 |                     },
 47 |                     "issue_type": {
 48 |                         "type": "string",
 49 |                         "enum": ["Story", "Bug", "Task", "Epic"],
 50 |                         "description": "Type of issue to create"
 51 |                     },
 52 |                     "priority": {
 53 |                         "type": "string",
 54 |                         "enum": ["Highest", "High", "Medium", "Low", "Lowest"],
 55 |                         "description": "Priority level"
 56 |                     },
 57 |                     "story_points": {
 58 |                         "type": "number",
 59 |                         "description": "Story points estimate (optional)"
 60 |                     },
 61 |                     "assignee": {
 62 |                         "type": "string",
 63 |                         "description": "Username to assign the issue to (optional)"
 64 |                     }
 65 |                 },
 66 |                 "required": ["summary", "description", "issue_type", "priority"]
 67 |             }
 68 |         ),
 69 |         Tool(
 70 |             name="search_issues",
 71 |             description="Search for Jira issues using JQL",
 72 |             inputSchema={
 73 |                 "type": "object",
 74 |                 "properties": {
 75 |                     "jql": {
 76 |                         "type": "string",
 77 |                         "description": "JQL query to search for issues"
 78 |                     },
 79 |                     "max_results": {
 80 |                         "type": "number",
 81 |                         "description": "Maximum number of results to return (default: 20)"
 82 |                     }
 83 |                 },
 84 |                 "required": ["jql"]
 85 |             }
 86 |         ),
 87 |         Tool(
 88 |             name="get_sprint_status",
 89 |             description="Get current sprint status and progress",
 90 |             inputSchema={
 91 |                 "type": "object",
 92 |                 "properties": {
 93 |                     "sprint_id": {
 94 |                         "type": "number",
 95 |                         "description": "Sprint ID to analyze (optional, defaults to active sprint)"
 96 |                     }
 97 |                 }
 98 |             }
 99 |         ),
100 |         Tool(
101 |             name="get_team_workload",
102 |             description="Analyze team workload and capacity",
103 |             inputSchema={
104 |                 "type": "object",
105 |                 "properties": {
106 |                     "team_members": {
107 |                         "type": "array",
108 |                         "items": {"type": "string"},
109 |                         "description": "List of team member usernames to analyze"
110 |                     }
111 |                 },
112 |                 "required": ["team_members"]
113 |             }
114 |         ),
115 |         Tool(
116 |             name="generate_standup_report",
117 |             description="Generate daily standup report for the active sprint",
118 |             inputSchema={
119 |                 "type": "object",
120 |                 "properties": {}
121 |             }
122 |         )
123 |     ]
124 | 
125 | @server.call_tool()
126 | async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
127 |     """Handle tool calls for Jira operations."""
128 |     if not jira_client:
129 |         return [TextContent(type="text", text="Error: Jira client not initialized")]
130 |     
131 |     try:
132 |         if name == "create_issue":
133 |             return await handle_create_issue(arguments)
134 |         elif name == "search_issues":
135 |             return await handle_search_issues(arguments)
136 |         elif name == "get_sprint_status":
137 |             return await handle_sprint_status(arguments)
138 |         elif name == "get_team_workload":
139 |             return await handle_team_workload(arguments)
140 |         elif name == "generate_standup_report":
141 |             return await handle_standup_report(arguments)
142 |         else:
143 |             return [TextContent(type="text", text=f"Unknown tool: {name}")]
144 |     
145 |     except Exception as e:
146 |         logger.exception(f"Error executing tool {name}: {str(e)}")
147 |         return [TextContent(type="text", text=f"Error: {str(e)}")]
148 | 
149 | # Tool handlers
150 | async def handle_create_issue(args: Dict[str, Any]) -> List[TextContent]:
151 |     """Handle create_issue tool call."""
152 |     issue_key = await jira_client.create_issue(
153 |         summary=args["summary"],
154 |         description=args["description"],
155 |         issue_type=IssueType(args["issue_type"]),
156 |         priority=Priority(args["priority"]),
157 |         story_points=args.get("story_points"),
158 |         assignee=args.get("assignee")
159 |     )
160 |     
161 |     return [TextContent(
162 |         type="text",
163 |         text=f"✅ Created issue {issue_key}: {args['summary']}"
164 |     )]
165 | 
166 | async def handle_search_issues(args: Dict[str, Any]) -> List[TextContent]:
167 |     """Handle search_issues tool call."""
168 |     jql = args["jql"]
169 |     max_results = args.get("max_results", 20)
170 |     
171 |     issues = await jira_client.search_issues(jql)
172 |     issues = issues[:max_results]  # Limit results
173 |     
174 |     if not issues:
175 |         return [TextContent(type="text", text="No issues found matching the query.")]
176 |     
177 |     # Format results
178 |     result_text = f"Found {len(issues)} issues:\n\n"
179 |     for issue in issues:
180 |         status_emoji = "✅" if issue.status.value == "Done" else "🔄" if issue.status.value == "In Progress" else "📋"
181 |         priority_emoji = "🔴" if issue.priority.value in ["Highest", "High"] else "🟡" if issue.priority.value == "Medium" else "🟢"
182 |         
183 |         assignee_text = f" (👤 {issue.assignee.display_name})" if issue.assignee else " (Unassigned)"
184 |         points_text = f" [{issue.story_points}pts]" if issue.story_points else ""
185 |         
186 |         result_text += f"{status_emoji} **{issue.key}**: {issue.summary}\n"
187 |         result_text += f"   {priority_emoji} {issue.priority.value} | {issue.status.value}{assignee_text}{points_text}\n\n"
188 |     
189 |     return [TextContent(type="text", text=result_text)]
190 | 
191 | async def handle_sprint_status(args: Dict[str, Any]) -> List[TextContent]:
192 |     """Handle get_sprint_status tool call."""
193 |     sprint_id = args.get("sprint_id")
194 |     
195 |     if sprint_id:
196 |         sprint = await jira_client.get_sprint(sprint_id)
197 |     else:
198 |         sprint = await jira_client.get_active_sprint()
199 |         if not sprint:
200 |             return [TextContent(type="text", text="No active sprint found.")]
201 |     
202 |     issues = await jira_client.get_sprint_issues(sprint.id)
203 |     
204 |     # Calculate metrics
205 |     total_points = sum(issue.story_points for issue in issues if issue.story_points)
206 |     completed_points = sum(issue.story_points for issue in issues 
207 |                           if issue.story_points and issue.status.value == "Done")
208 |     in_progress_count = len([i for i in issues if i.status.value == "In Progress"])
209 |     blocked_count = len([i for i in issues if i.status.value == "Blocked"])
210 |     
211 |     completion_rate = (completed_points / total_points * 100) if total_points > 0 else 0
212 |     
213 |     # Build report
214 |     report = f"## 📊 Sprint Status: {sprint.name}\n\n"
215 |     report += f"**Status**: {sprint.status.value}\n"
216 |     report += f"**Goal**: {sprint.goal or 'No goal set'}\n"
217 |     if sprint.start_date and sprint.end_date:
218 |         days_remaining = (sprint.end_date - datetime.now()).days
219 |         report += f"**Duration**: {sprint.start_date.strftime('%Y-%m-%d')} to {sprint.end_date.strftime('%Y-%m-%d')}\n"
220 |         report += f"**Days Remaining**: {max(0, days_remaining)}\n"
221 |     
222 |     report += f"\n### 📈 Progress\n"
223 |     report += f"- **Completion**: {completion_rate:.1f}% ({completed_points}/{total_points} points)\n"
224 |     report += f"- **Total Issues**: {len(issues)}\n"
225 |     report += f"- **In Progress**: {in_progress_count}\n"
226 |     if blocked_count > 0:
227 |         report += f"- **⚠️ Blocked**: {blocked_count}\n"
228 |     
229 |     return [TextContent(type="text", text=report)]
230 | 
231 | async def handle_team_workload(args: Dict[str, Any]) -> List[TextContent]:
232 |     """Handle get_team_workload tool call."""
233 |     team_members = args["team_members"]
234 |     
235 |     report = "## 👥 Team Workload Analysis\n\n"
236 |     
237 |     for member in team_members:
238 |         try:
239 |             issues = await jira_client.get_assigned_issues(member)
240 |             total_points = sum(issue.story_points for issue in issues if issue.story_points)
241 |             in_progress_count = len([i for i in issues if i.status.value == "In Progress"])
242 |             
243 |             workload_emoji = "🔴" if total_points > 15 else "🟡" if total_points > 10 else "🟢"
244 |             
245 |             report += f"### {workload_emoji} {member}\n"
246 |             report += f"- **Total Points**: {total_points}\n"
247 |             report += f"- **Active Issues**: {in_progress_count}\n"
248 |             report += f"- **Total Issues**: {len(issues)}\n\n"
249 |             
250 |         except Exception as e:
251 |             report += f"### ❌ {member}\n"
252 |             report += f"- **Error**: Could not fetch data ({str(e)})\n\n"
253 |     
254 |     return [TextContent(type="text", text=report)]
255 | 
256 | async def handle_standup_report(args: Dict[str, Any]) -> List[TextContent]:
257 |     """Handle generate_standup_report tool call."""
258 |     active_sprint = await jira_client.get_active_sprint()
259 |     if not active_sprint:
260 |         return [TextContent(type="text", text="No active sprint found for standup report.")]
261 |     
262 |     issues = await jira_client.get_sprint_issues(active_sprint.id)
263 |     
264 |     # Categorize issues
265 |     yesterday = datetime.now().date()
266 |     completed_yesterday = [i for i in issues if i.status.value == "Done" and i.updated_at.date() == yesterday]
267 |     in_progress = [i for i in issues if i.status.value == "In Progress"]
268 |     blocked = [i for i in issues if i.status.value == "Blocked"]
269 |     
270 |     report = f"## 🌅 Daily Standup - {datetime.now().strftime('%Y-%m-%d')}\n\n"
271 |     report += f"**Sprint**: {active_sprint.name}\n\n"
272 |     
273 |     if completed_yesterday:
274 |         report += "### ✅ Completed Yesterday\n"
275 |         for issue in completed_yesterday:
276 |             assignee = issue.assignee.display_name if issue.assignee else "Unassigned"
277 |             report += f"- **{issue.key}**: {issue.summary} ({assignee})\n"
278 |         report += "\n"
279 |     
280 |     if in_progress:
281 |         report += "### 🔄 In Progress\n"
282 |         for issue in in_progress:
283 |             assignee = issue.assignee.display_name if issue.assignee else "Unassigned"
284 |             points = f" [{issue.story_points}pts]" if issue.story_points else ""
285 |             report += f"- **{issue.key}**: {issue.summary} ({assignee}){points}\n"
286 |         report += "\n"
287 |     
288 |     if blocked:
289 |         report += "### ⚠️ Blocked Issues\n"
290 |         for issue in blocked:
291 |             assignee = issue.assignee.display_name if issue.assignee else "Unassigned"
292 |             report += f"- **{issue.key}**: {issue.summary} ({assignee})\n"
293 |         report += "\n"
294 |     
295 |     # Add quick metrics
296 |     total_points = sum(i.story_points for i in issues if i.story_points)
297 |     completed_points = sum(i.story_points for i in issues if i.story_points and i.status.value == "Done")
298 |     
299 |     report += "### 📊 Sprint Metrics\n"
300 |     report += f"- **Progress**: {completed_points}/{total_points} points ({(completed_points/total_points*100):.1f}%)\n"
301 |     report += f"- **Active Issues**: {len(in_progress)}\n"
302 |     if blocked:
303 |         report += f"- **Blocked Issues**: {len(blocked)} ⚠️\n"
304 |     
305 |     return [TextContent(type="text", text=report)]
306 | 
307 | async def main():
308 |     """Main entry point for the MCP server."""
309 |     global jira_client
310 |     
311 |     # Initialize settings and Jira client
312 |     settings = get_settings()
313 |     jira_client = JiraClient(settings)
314 |     
315 |     logger.info("Starting MCP Jira server...")
316 |     
317 |     # Run the MCP server
318 |     async with stdio_server() as (read_stream, write_stream):
319 |         await server.run(read_stream, write_stream, server.create_initialization_options())
320 | 
321 | if __name__ == "__main__":
322 |     asyncio.run(main())
```