jira-mcp-tools 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ package-lock.json linguist-generated=true
@@ -0,0 +1,50 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+ env/
28
+
29
+ # Environment variables
30
+ .env
31
+ .env.local
32
+
33
+ # IDE
34
+ .vscode/
35
+ .idea/
36
+ *.swp
37
+ *.swo
38
+ *~
39
+
40
+ # OS
41
+ .DS_Store
42
+ Thumbs.db
43
+
44
+ # Testing
45
+ .pytest_cache/
46
+ .coverage
47
+ htmlcov/
48
+
49
+ # UV
50
+ uv.lock
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 IBM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: jira-mcp-tools
3
+ Version: 0.2.0
4
+ Summary: Model Context Protocol server for Jira integration
5
+ Project-URL: Homepage, https://github.com/IBM/jira-mcp-tools
6
+ Project-URL: Repository, https://github.com/IBM/jira-mcp-tools
7
+ Project-URL: Issues, https://github.com/IBM/jira-mcp-tools/issues
8
+ Author-email: IBM <opensource@ibm.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,jira,llm,mcp,model-context-protocol
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: click>=8.0.0
22
+ Requires-Dist: fastmcp>=2.6.1
23
+ Requires-Dist: jira>=3.4.0
24
+ Requires-Dist: python-dotenv>=1.0.0
25
+ Requires-Dist: requests>=2.32.5
26
+ Requires-Dist: ruff>=0.11.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # MCP Server for JIRA
30
+
31
+ A Model Context Protocol (MCP) server that provides seamless integration with Jira. This server enables AI assistants to retrieve issue information, comments, and attachments from any Jira instance.
32
+
33
+ ## Features
34
+
35
+ - 🎫 **Get Issue Information**: Retrieve detailed issue descriptions and comments
36
+ - 📎 **Download Attachments**: Access and download issue attachments
37
+ - 🔒 **Secure Authentication**: Uses Jira API tokens for secure access
38
+ - 🌐 **Universal Compatibility**: Works with Jira Cloud, Server, and Data Center
39
+
40
+ ## Installation
41
+
42
+ ### Using uvx (Recommended)
43
+
44
+ ```bash
45
+ uvx mcp-jira
46
+ ```
47
+
48
+ ### Using pip
49
+
50
+ ```bash
51
+ pip install mcp-jira
52
+ ```
53
+
54
+ ## Prerequisites
55
+
56
+ - Python 3.10 or higher
57
+ - Jira account with API access
58
+ - Jira API token ([How to create](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/))
59
+
60
+ ## Configuration
61
+
62
+ ### Environment Variables
63
+
64
+ The server requires three environment variables:
65
+
66
+ - `JIRA_URL`: Your Jira instance URL (e.g., `https://your-company.atlassian.net/`)
67
+ - `JIRA_EMAIL`: Your Jira account email
68
+ - `JIRA_TOKEN`: Your Jira API token
69
+
70
+ ### MCP Client Configuration
71
+
72
+ Add to your MCP client configuration (e.g., Claude Desktop, Bob):
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "jira": {
78
+ "command": "uvx",
79
+ "args": ["mcp-jira"],
80
+ "env": {
81
+ "JIRA_URL": "https://your-jira-instance.com/",
82
+ "JIRA_EMAIL": "your-email@company.com",
83
+ "JIRA_TOKEN": "your-api-token"
84
+ }
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ ## Available Tools
91
+
92
+ ### `get_jira_ticket_info`
93
+
94
+ Retrieves comprehensive information about a Jira issue.
95
+
96
+ **Parameters:**
97
+ - `issue_key` (string): The Jira issue key (e.g., "PROJ-123")
98
+
99
+ **Returns:**
100
+ - Issue description
101
+ - All comments with author and timestamp
102
+ - Comment bodies
103
+
104
+ **Example:**
105
+ ```python
106
+ get_jira_ticket_info("PROJ-123")
107
+ ```
108
+
109
+ ### `get_jira_ticket_attachments`
110
+
111
+ Downloads and retrieves attachments from a Jira issue.
112
+
113
+ **Parameters:**
114
+ - `issue_key` (string): The Jira issue key (e.g., "PROJ-123")
115
+
116
+ **Returns:**
117
+ - List of attachments with filenames and content
118
+
119
+ **Example:**
120
+ ```python
121
+ get_jira_ticket_attachments("PROJ-456")
122
+ ```
123
+
124
+ ## Development
125
+
126
+ ### Setup
127
+
128
+ 1. Clone the repository
129
+ 2. Install dependencies:
130
+ ```bash
131
+ uv sync
132
+ ```
133
+
134
+ 3. Create a `.env` file based on `env.template`:
135
+ ```bash
136
+ cp env.template .env
137
+ # Edit .env with your credentials
138
+ ```
139
+
140
+ ### Running Locally
141
+
142
+ ```bash
143
+ uv run server.py
144
+ ```
145
+
146
+ ### Testing
147
+
148
+ ```bash
149
+ # Run tests
150
+ uv run pytest
151
+
152
+ # Run with coverage
153
+ uv run pytest --cov
154
+ ```
155
+
156
+ ## Use Cases
157
+
158
+ - **AI-Assisted Development**: Let AI assistants fetch and analyze Jira issues
159
+ - **Automated Workflows**: Integrate Jira data into automated processes
160
+ - **Context-Aware Coding**: Provide issue context to AI coding assistants
161
+ - **Documentation**: Auto-generate documentation from Jira issues
162
+
163
+ ## Roadmap
164
+
165
+ - [ ] Support for creating and updating issues
166
+ - [ ] Advanced search capabilities
167
+ - [ ] Support for Jira workflows and transitions
168
+ - [ ] Enhanced attachment handling (PDFs, images)
169
+ - [ ] Bulk operations support
170
+
171
+ ## Contributing
172
+
173
+ Contributions are welcome! Please feel free to submit a Pull Request.
174
+
175
+ ## License
176
+
177
+ MIT License - see [LICENSE](LICENSE) file for details.
178
+
179
+ ## Support
180
+
181
+ - **Issues**: [GitHub Issues](https://github.com/IBM/mcp-jira/issues)
182
+ - **Documentation**: [Full Documentation](https://github.com/IBM/mcp-jira)
183
+
184
+ ## Acknowledgments
185
+
186
+ Built with:
187
+ - [FastMCP](https://github.com/jlowin/fastmcp) - Fast MCP server framework
188
+ - [jira-python](https://github.com/pycontribs/jira) - Python Jira library
189
+ - [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification
@@ -0,0 +1,161 @@
1
+ # Publishing mcp-jira to PyPI
2
+
3
+ This guide explains how to publish the mcp-jira package to PyPI.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. **PyPI Account**
8
+ - Create account at https://pypi.org/account/register/
9
+ - Verify your email address
10
+
11
+ 2. **PyPI API Token**
12
+ - Go to https://pypi.org/manage/account/token/
13
+ - Create a new API token with scope "Entire account"
14
+ - Save the token securely (starts with `pypi-`)
15
+
16
+ 3. **Install Build Tools**
17
+ ```bash
18
+ pip install build twine
19
+ ```
20
+
21
+ ## Publishing Steps
22
+
23
+ ### 1. Prepare the Release
24
+
25
+ Update version in `pyproject.toml`:
26
+ ```toml
27
+ version = "0.1.2" # Increment version
28
+ ```
29
+
30
+ ### 2. Build the Package
31
+
32
+ ```bash
33
+ cd /path/to/mcp-jira
34
+ python -m build
35
+ ```
36
+
37
+ This creates:
38
+ - `dist/mcp_jira-0.1.2-py3-none-any.whl`
39
+ - `dist/mcp-jira-0.1.2.tar.gz`
40
+
41
+ ### 3. Test on TestPyPI (Optional but Recommended)
42
+
43
+ ```bash
44
+ # Upload to TestPyPI
45
+ twine upload --repository testpypi dist/*
46
+
47
+ # Test installation
48
+ pip install --index-url https://test.pypi.org/simple/ mcp-jira
49
+ ```
50
+
51
+ ### 4. Upload to PyPI
52
+
53
+ ```bash
54
+ twine upload dist/*
55
+ ```
56
+
57
+ When prompted:
58
+ - Username: `__token__`
59
+ - Password: Your PyPI API token (including `pypi-` prefix)
60
+
61
+ ### 5. Verify Installation
62
+
63
+ ```bash
64
+ # Install from PyPI
65
+ pip install mcp-jira
66
+
67
+ # Or with uvx
68
+ uvx mcp-jira
69
+ ```
70
+
71
+ ## Using API Token in CI/CD
72
+
73
+ For automated publishing, store the token as a secret:
74
+
75
+ ### GitHub Actions
76
+
77
+ ```yaml
78
+ name: Publish to PyPI
79
+
80
+ on:
81
+ release:
82
+ types: [published]
83
+
84
+ jobs:
85
+ publish:
86
+ runs-on: ubuntu-latest
87
+ steps:
88
+ - uses: actions/checkout@v3
89
+
90
+ - name: Set up Python
91
+ uses: actions/setup-python@v4
92
+ with:
93
+ python-version: '3.10'
94
+
95
+ - name: Install dependencies
96
+ run: |
97
+ pip install build twine
98
+
99
+ - name: Build package
100
+ run: python -m build
101
+
102
+ - name: Publish to PyPI
103
+ env:
104
+ TWINE_USERNAME: __token__
105
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
106
+ run: twine upload dist/*
107
+ ```
108
+
109
+ ## Version Management
110
+
111
+ Follow semantic versioning (MAJOR.MINOR.PATCH):
112
+
113
+ - **MAJOR**: Breaking changes
114
+ - **MINOR**: New features (backward compatible)
115
+ - **PATCH**: Bug fixes
116
+
117
+ Examples:
118
+ - `0.1.1` → `0.1.2` (bug fix)
119
+ - `0.1.2` → `0.2.0` (new feature)
120
+ - `0.2.0` → `1.0.0` (stable release)
121
+
122
+ ## Troubleshooting
123
+
124
+ ### "File already exists" Error
125
+
126
+ You cannot re-upload the same version. Increment the version number.
127
+
128
+ ### Authentication Failed
129
+
130
+ - Ensure username is `__token__`
131
+ - Verify token includes `pypi-` prefix
132
+ - Check token hasn't expired
133
+
134
+ ### Package Name Conflict
135
+
136
+ If `mcp-jira` is taken, choose alternative:
137
+ - `mcp-jira-server`
138
+ - `jira-mcp-server`
139
+ - `ibm-mcp-jira`
140
+
141
+ ## Post-Publishing
142
+
143
+ 1. **Create GitHub Release**
144
+ - Tag: `v0.1.2`
145
+ - Title: `Release 0.1.2`
146
+ - Description: Changelog
147
+
148
+ 2. **Update Documentation**
149
+ - Update README with new version
150
+ - Update bob-marketplace-registry
151
+
152
+ 3. **Announce**
153
+ - Internal Slack channels
154
+ - GitHub discussions
155
+ - Documentation sites
156
+
157
+ ## Resources
158
+
159
+ - [PyPI Help](https://pypi.org/help/)
160
+ - [Python Packaging Guide](https://packaging.python.org/)
161
+ - [Twine Documentation](https://twine.readthedocs.io/)
@@ -0,0 +1,161 @@
1
+ # MCP Server for JIRA
2
+
3
+ A Model Context Protocol (MCP) server that provides seamless integration with Jira. This server enables AI assistants to retrieve issue information, comments, and attachments from any Jira instance.
4
+
5
+ ## Features
6
+
7
+ - 🎫 **Get Issue Information**: Retrieve detailed issue descriptions and comments
8
+ - 📎 **Download Attachments**: Access and download issue attachments
9
+ - 🔒 **Secure Authentication**: Uses Jira API tokens for secure access
10
+ - 🌐 **Universal Compatibility**: Works with Jira Cloud, Server, and Data Center
11
+
12
+ ## Installation
13
+
14
+ ### Using uvx (Recommended)
15
+
16
+ ```bash
17
+ uvx mcp-jira
18
+ ```
19
+
20
+ ### Using pip
21
+
22
+ ```bash
23
+ pip install mcp-jira
24
+ ```
25
+
26
+ ## Prerequisites
27
+
28
+ - Python 3.10 or higher
29
+ - Jira account with API access
30
+ - Jira API token ([How to create](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/))
31
+
32
+ ## Configuration
33
+
34
+ ### Environment Variables
35
+
36
+ The server requires three environment variables:
37
+
38
+ - `JIRA_URL`: Your Jira instance URL (e.g., `https://your-company.atlassian.net/`)
39
+ - `JIRA_EMAIL`: Your Jira account email
40
+ - `JIRA_TOKEN`: Your Jira API token
41
+
42
+ ### MCP Client Configuration
43
+
44
+ Add to your MCP client configuration (e.g., Claude Desktop, Bob):
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "jira": {
50
+ "command": "uvx",
51
+ "args": ["mcp-jira"],
52
+ "env": {
53
+ "JIRA_URL": "https://your-jira-instance.com/",
54
+ "JIRA_EMAIL": "your-email@company.com",
55
+ "JIRA_TOKEN": "your-api-token"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## Available Tools
63
+
64
+ ### `get_jira_ticket_info`
65
+
66
+ Retrieves comprehensive information about a Jira issue.
67
+
68
+ **Parameters:**
69
+ - `issue_key` (string): The Jira issue key (e.g., "PROJ-123")
70
+
71
+ **Returns:**
72
+ - Issue description
73
+ - All comments with author and timestamp
74
+ - Comment bodies
75
+
76
+ **Example:**
77
+ ```python
78
+ get_jira_ticket_info("PROJ-123")
79
+ ```
80
+
81
+ ### `get_jira_ticket_attachments`
82
+
83
+ Downloads and retrieves attachments from a Jira issue.
84
+
85
+ **Parameters:**
86
+ - `issue_key` (string): The Jira issue key (e.g., "PROJ-123")
87
+
88
+ **Returns:**
89
+ - List of attachments with filenames and content
90
+
91
+ **Example:**
92
+ ```python
93
+ get_jira_ticket_attachments("PROJ-456")
94
+ ```
95
+
96
+ ## Development
97
+
98
+ ### Setup
99
+
100
+ 1. Clone the repository
101
+ 2. Install dependencies:
102
+ ```bash
103
+ uv sync
104
+ ```
105
+
106
+ 3. Create a `.env` file based on `env.template`:
107
+ ```bash
108
+ cp env.template .env
109
+ # Edit .env with your credentials
110
+ ```
111
+
112
+ ### Running Locally
113
+
114
+ ```bash
115
+ uv run server.py
116
+ ```
117
+
118
+ ### Testing
119
+
120
+ ```bash
121
+ # Run tests
122
+ uv run pytest
123
+
124
+ # Run with coverage
125
+ uv run pytest --cov
126
+ ```
127
+
128
+ ## Use Cases
129
+
130
+ - **AI-Assisted Development**: Let AI assistants fetch and analyze Jira issues
131
+ - **Automated Workflows**: Integrate Jira data into automated processes
132
+ - **Context-Aware Coding**: Provide issue context to AI coding assistants
133
+ - **Documentation**: Auto-generate documentation from Jira issues
134
+
135
+ ## Roadmap
136
+
137
+ - [ ] Support for creating and updating issues
138
+ - [ ] Advanced search capabilities
139
+ - [ ] Support for Jira workflows and transitions
140
+ - [ ] Enhanced attachment handling (PDFs, images)
141
+ - [ ] Bulk operations support
142
+
143
+ ## Contributing
144
+
145
+ Contributions are welcome! Please feel free to submit a Pull Request.
146
+
147
+ ## License
148
+
149
+ MIT License - see [LICENSE](LICENSE) file for details.
150
+
151
+ ## Support
152
+
153
+ - **Issues**: [GitHub Issues](https://github.com/IBM/mcp-jira/issues)
154
+ - **Documentation**: [Full Documentation](https://github.com/IBM/mcp-jira)
155
+
156
+ ## Acknowledgments
157
+
158
+ Built with:
159
+ - [FastMCP](https://github.com/jlowin/fastmcp) - Fast MCP server framework
160
+ - [jira-python](https://github.com/pycontribs/jira) - Python Jira library
161
+ - [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification
@@ -0,0 +1,3 @@
1
+ JIRA_URL=https://your-jira-instance.com/
2
+ JIRA_EMAIL=your-email@company.com
3
+ JIRA_TOKEN=your-api-token
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "jira-mcp-tools"
3
+ version = "0.2.0"
4
+ description = "Model Context Protocol server for Jira integration"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = {text = "MIT"}
8
+ authors = [
9
+ {name = "IBM", email = "opensource@ibm.com"}
10
+ ]
11
+ keywords = ["mcp", "jira", "model-context-protocol", "ai", "llm"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+ dependencies = [
23
+ "fastmcp>=2.6.1",
24
+ "click>=8.0.0",
25
+ "ruff>=0.11.0",
26
+ "python-dotenv>=1.0.0",
27
+ "requests>=2.32.5",
28
+ "jira>=3.4.0"
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/IBM/jira-mcp-tools"
33
+ Repository = "https://github.com/IBM/jira-mcp-tools"
34
+ Issues = "https://github.com/IBM/jira-mcp-tools/issues"
35
+
36
+ [project.scripts]
37
+ jira-mcp-tools = "mcp-jira.server:main"
38
+
39
+ [build-system]
40
+ requires = ["hatchling"]
41
+ build-backend = "hatchling.build"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/mcp-jira"]
@@ -0,0 +1,7 @@
1
+ from . import server
2
+
3
+ def main():
4
+ server.main()
5
+
6
+ # Optionally expose other important items at package level
7
+ __all__ = ['main', 'server']
@@ -0,0 +1,745 @@
1
+ import os
2
+ import json
3
+ import requests
4
+ from fastmcp import FastMCP, Context
5
+ from contextlib import asynccontextmanager
6
+ from typing import AsyncIterator, Optional
7
+ from jira import JIRA
8
+
9
+ class JiraApiClient:
10
+ def __init__(self, server_url, email, api_token):
11
+ self.jira = JIRA(server=server_url, basic_auth=(email, api_token))
12
+
13
+ async def get_jira_ticket_info(self, issue_key: str):
14
+ """Get information about a JIRA issue."""
15
+ try:
16
+ # Check if we have a valid cached response
17
+ issue = self.jira.issue(issue_key)
18
+ # Get description
19
+ description = issue.fields.description or "No description"
20
+
21
+ # Get comments
22
+ comments = []
23
+ for comment in issue.fields.comment.comments:
24
+ comments.append({
25
+ 'author': comment.author.displayName,
26
+ 'created': comment.created,
27
+ 'body': comment.body
28
+ })
29
+
30
+ result = {
31
+ 'description': description,
32
+ 'comments': comments
33
+ }
34
+
35
+ #TODO: handle comments with images
36
+
37
+ return json.dumps(result, indent=2)
38
+
39
+ except requests.exceptions.RequestException as e:
40
+ # Handle network errors, timeouts, etc.
41
+ raise ValueError(f"Failed to connect to JIRA API: {str(e)}")
42
+ except json.JSONDecodeError as e:
43
+ # Handle malformed JSON responses
44
+ raise ValueError(f"Failed to parse JIRA API response: {str(e)}")
45
+ except Exception as e:
46
+ raise ValueError(f"Failed to get JIRA info: {str(e)}")
47
+
48
+ async def get_jira_ticket_attachments(self, issue_key: str):
49
+ """Get the attachments of a JIRA issue."""
50
+ try:
51
+ issue = self.jira.issue(issue_key)
52
+ attachments = []
53
+ for attachment in issue.fields.attachment:
54
+ attachments.append({
55
+ 'filename': attachment.filename,
56
+ 'content': attachment.get()
57
+ })
58
+
59
+ #TODO: handle pdf and and image attachments
60
+
61
+ return attachments
62
+ except requests.exceptions.RequestException as e:
63
+ # Handle network errors, timeouts, etc.
64
+ raise ValueError(f"Failed to connect to JIRA API: {str(e)}")
65
+ except json.JSONDecodeError as e:
66
+ # Handle malformed JSON responses
67
+ raise ValueError(f"Failed to parse JIRA API response: {str(e)}")
68
+ except Exception as e:
69
+ raise ValueError(f"Failed to get JIRA info: {str(e)}")
70
+
71
+ async def search_issues(self, jql: str, max_results: int = 50, fields: Optional[str] = None):
72
+ """Search for issues using JQL (Jira Query Language)."""
73
+ try:
74
+ # Default fields if not specified
75
+ if not fields:
76
+ fields = "summary,status,assignee,priority,created,updated,duedate,labels,components"
77
+
78
+ issues = self.jira.search_issues(jql, maxResults=max_results, fields=fields)
79
+
80
+ results = []
81
+ for issue in issues:
82
+ issue_data = {
83
+ 'key': issue.key,
84
+ 'summary': issue.fields.summary,
85
+ 'status': issue.fields.status.name,
86
+ 'assignee': issue.fields.assignee.displayName if issue.fields.assignee else 'Unassigned',
87
+ 'priority': issue.fields.priority.name if issue.fields.priority else 'None',
88
+ 'created': issue.fields.created,
89
+ 'updated': issue.fields.updated,
90
+ 'duedate': issue.fields.duedate if hasattr(issue.fields, 'duedate') else None,
91
+ 'labels': issue.fields.labels if hasattr(issue.fields, 'labels') else [],
92
+ 'components': [c.name for c in issue.fields.components] if hasattr(issue.fields, 'components') else []
93
+ }
94
+ results.append(issue_data)
95
+
96
+ return json.dumps({
97
+ 'total': len(results),
98
+ 'issues': results
99
+ }, indent=2)
100
+
101
+ except Exception as e:
102
+ raise ValueError(f"Failed to search issues: {str(e)}")
103
+
104
+ async def get_epic_details(self, epic_key: str):
105
+ """Get detailed information about an epic including all child issues."""
106
+ try:
107
+ epic = self.jira.issue(epic_key)
108
+
109
+ # Get all issues in this epic using JQL
110
+ jql = f'"Epic Link" = {epic_key}'
111
+ child_issues = self.jira.search_issues(jql, maxResults=1000)
112
+
113
+ # Calculate progress metrics
114
+ total_issues = len(child_issues)
115
+ done_issues = sum(1 for issue in child_issues if issue.fields.status.name.lower() in ['done', 'closed', 'resolved'])
116
+ in_progress = sum(1 for issue in child_issues if issue.fields.status.name.lower() in ['in progress', 'in review'])
117
+
118
+ # Get story points if available
119
+ total_points = 0
120
+ done_points = 0
121
+ for issue in child_issues:
122
+ # Story points field varies by Jira instance (customfield_10016 is common)
123
+ if hasattr(issue.fields, 'customfield_10016') and issue.fields.customfield_10016:
124
+ points = float(issue.fields.customfield_10016)
125
+ total_points += points
126
+ if issue.fields.status.name.lower() in ['done', 'closed', 'resolved']:
127
+ done_points += points
128
+
129
+ # Build child issues list
130
+ children = []
131
+ for issue in child_issues:
132
+ children.append({
133
+ 'key': issue.key,
134
+ 'summary': issue.fields.summary,
135
+ 'status': issue.fields.status.name,
136
+ 'assignee': issue.fields.assignee.displayName if issue.fields.assignee else 'Unassigned',
137
+ 'type': issue.fields.issuetype.name
138
+ })
139
+
140
+ result = {
141
+ 'epic_key': epic_key,
142
+ 'epic_summary': epic.fields.summary,
143
+ 'epic_status': epic.fields.status.name,
144
+ 'description': epic.fields.description or 'No description',
145
+ 'total_issues': total_issues,
146
+ 'done_issues': done_issues,
147
+ 'in_progress_issues': in_progress,
148
+ 'todo_issues': total_issues - done_issues - in_progress,
149
+ 'completion_percentage': round((done_issues / total_issues * 100) if total_issues > 0 else 0, 1),
150
+ 'total_story_points': total_points,
151
+ 'done_story_points': done_points,
152
+ 'child_issues': children
153
+ }
154
+
155
+ return json.dumps(result, indent=2)
156
+
157
+ except Exception as e:
158
+ raise ValueError(f"Failed to get epic details: {str(e)}")
159
+
160
+ async def get_sprint_info(self, board_id: int, sprint_id: Optional[int] = None):
161
+ """Get information about sprints on a board."""
162
+ try:
163
+ if sprint_id:
164
+ # Get specific sprint
165
+ sprint = self.jira.sprint(sprint_id)
166
+ issues = self.jira.search_issues(f'sprint = {sprint_id}', maxResults=1000)
167
+
168
+ sprint_data = {
169
+ 'id': sprint.id,
170
+ 'name': sprint.name,
171
+ 'state': sprint.state,
172
+ 'start_date': sprint.startDate if hasattr(sprint, 'startDate') else None,
173
+ 'end_date': sprint.endDate if hasattr(sprint, 'endDate') else None,
174
+ 'total_issues': len(issues),
175
+ 'issues': []
176
+ }
177
+
178
+ for issue in issues:
179
+ sprint_data['issues'].append({
180
+ 'key': issue.key,
181
+ 'summary': issue.fields.summary,
182
+ 'status': issue.fields.status.name,
183
+ 'assignee': issue.fields.assignee.displayName if issue.fields.assignee else 'Unassigned'
184
+ })
185
+
186
+ return json.dumps(sprint_data, indent=2)
187
+ else:
188
+ # Get all sprints for board
189
+ sprints = self.jira.sprints(board_id)
190
+
191
+ sprint_list = []
192
+ for sprint in sprints:
193
+ sprint_list.append({
194
+ 'id': sprint.id,
195
+ 'name': sprint.name,
196
+ 'state': sprint.state,
197
+ 'start_date': sprint.startDate if hasattr(sprint, 'startDate') else None,
198
+ 'end_date': sprint.endDate if hasattr(sprint, 'endDate') else None
199
+ })
200
+
201
+ return json.dumps({
202
+ 'board_id': board_id,
203
+ 'total_sprints': len(sprint_list),
204
+ 'sprints': sprint_list
205
+ }, indent=2)
206
+
207
+ except Exception as e:
208
+ raise ValueError(f"Failed to get sprint info: {str(e)}")
209
+
210
+
211
+ async def get_issue_history(self, issue_key: str):
212
+ """Get the change history of a Jira issue."""
213
+ try:
214
+ issue = self.jira.issue(issue_key, expand='changelog')
215
+
216
+ history = []
217
+ for change in issue.changelog.histories:
218
+ change_data = {
219
+ 'author': change.author.displayName,
220
+ 'created': change.created,
221
+ 'changes': []
222
+ }
223
+
224
+ for item in change.items:
225
+ change_data['changes'].append({
226
+ 'field': item.field,
227
+ 'fieldtype': item.fieldtype,
228
+ 'from': item.fromString,
229
+ 'to': item.toString
230
+ })
231
+
232
+ history.append(change_data)
233
+
234
+ result = {
235
+ 'issue_key': issue_key,
236
+ 'summary': issue.fields.summary,
237
+ 'current_status': issue.fields.status.name,
238
+ 'history': history
239
+ }
240
+
241
+ return json.dumps(result, indent=2)
242
+
243
+ except Exception as e:
244
+ raise ValueError(f"Failed to get issue history: {str(e)}")
245
+
246
+ async def get_project_releases(self, project_key: str):
247
+ """Get all releases/versions for a project."""
248
+ try:
249
+ versions = self.jira.project_versions(project_key)
250
+
251
+ releases = []
252
+ for version in versions:
253
+ # Get issues for this version
254
+ jql = f'project = {project_key} AND fixVersion = "{version.name}"'
255
+ issues = self.jira.search_issues(jql, maxResults=1000)
256
+
257
+ release_data = {
258
+ 'id': version.id,
259
+ 'name': version.name,
260
+ 'description': version.description if hasattr(version, 'description') else None,
261
+ 'released': version.released if hasattr(version, 'released') else False,
262
+ 'release_date': version.releaseDate if hasattr(version, 'releaseDate') else None,
263
+ 'start_date': version.startDate if hasattr(version, 'startDate') else None,
264
+ 'archived': version.archived if hasattr(version, 'archived') else False,
265
+ 'total_issues': len(issues),
266
+ 'issues': []
267
+ }
268
+
269
+ # Add issue summaries
270
+ for issue in issues[:50]: # Limit to first 50 issues
271
+ release_data['issues'].append({
272
+ 'key': issue.key,
273
+ 'summary': issue.fields.summary,
274
+ 'status': issue.fields.status.name,
275
+ 'type': issue.fields.issuetype.name
276
+ })
277
+
278
+ releases.append(release_data)
279
+
280
+ return json.dumps({
281
+ 'project_key': project_key,
282
+ 'total_releases': len(releases),
283
+ 'releases': releases
284
+ }, indent=2)
285
+
286
+ except Exception as e:
287
+ raise ValueError(f"Failed to get project releases: {str(e)}")
288
+
289
+ async def get_issue_links(self, issue_key: str):
290
+ """Get all linked issues for a given issue."""
291
+ try:
292
+ issue = self.jira.issue(issue_key)
293
+
294
+ links = []
295
+ if hasattr(issue.fields, 'issuelinks'):
296
+ for link in issue.fields.issuelinks:
297
+ link_data = {
298
+ 'type': link.type.name,
299
+ 'direction': None,
300
+ 'linked_issue': {}
301
+ }
302
+
303
+ # Determine direction and get linked issue
304
+ if hasattr(link, 'outwardIssue'):
305
+ link_data['direction'] = 'outward'
306
+ linked = link.outwardIssue
307
+ elif hasattr(link, 'inwardIssue'):
308
+ link_data['direction'] = 'inward'
309
+ linked = link.inwardIssue
310
+ else:
311
+ continue
312
+
313
+ link_data['linked_issue'] = {
314
+ 'key': linked.key,
315
+ 'summary': linked.fields.summary,
316
+ 'status': linked.fields.status.name,
317
+ 'type': linked.fields.issuetype.name
318
+ }
319
+
320
+ links.append(link_data)
321
+
322
+ result = {
323
+ 'issue_key': issue_key,
324
+ 'summary': issue.fields.summary,
325
+ 'total_links': len(links),
326
+ 'links': links
327
+ }
328
+
329
+ return json.dumps(result, indent=2)
330
+
331
+ except Exception as e:
332
+ raise ValueError(f"Failed to get issue links: {str(e)}")
333
+
334
+ async def add_comment(self, issue_key: str, comment_text: str):
335
+ """Add a comment to a Jira issue."""
336
+ try:
337
+ issue = self.jira.issue(issue_key)
338
+ comment = self.jira.add_comment(issue, comment_text)
339
+
340
+ result = {
341
+ 'issue_key': issue_key,
342
+ 'comment_id': comment.id,
343
+ 'author': comment.author.displayName,
344
+ 'created': comment.created,
345
+ 'body': comment.body
346
+ }
347
+
348
+ return json.dumps(result, indent=2)
349
+
350
+ except Exception as e:
351
+ raise ValueError(f"Failed to add comment: {str(e)}")
352
+
353
+ async def update_issue(self, issue_key: str, fields: dict):
354
+ """Update fields of a Jira issue."""
355
+ try:
356
+ issue = self.jira.issue(issue_key)
357
+ issue.update(fields=fields)
358
+
359
+ # Get updated issue
360
+ updated_issue = self.jira.issue(issue_key)
361
+
362
+ result = {
363
+ 'issue_key': issue_key,
364
+ 'summary': updated_issue.fields.summary,
365
+ 'status': updated_issue.fields.status.name,
366
+ 'updated': updated_issue.fields.updated,
367
+ 'updated_fields': list(fields.keys())
368
+ }
369
+
370
+ return json.dumps(result, indent=2)
371
+
372
+ except Exception as e:
373
+ raise ValueError(f"Failed to update issue: {str(e)}")
374
+
375
+ async def create_issue(self, project_key: str, summary: str, description: str, issue_type: str = "Task"):
376
+ """Create a new Jira issue."""
377
+ try:
378
+ issue_dict = {
379
+ 'project': {'key': project_key},
380
+ 'summary': summary,
381
+ 'description': description,
382
+ 'issuetype': {'name': issue_type}
383
+ }
384
+
385
+ new_issue = self.jira.create_issue(fields=issue_dict)
386
+
387
+ result = {
388
+ 'issue_key': new_issue.key,
389
+ 'summary': new_issue.fields.summary,
390
+ 'status': new_issue.fields.status.name,
391
+ 'issue_type': new_issue.fields.issuetype.name,
392
+ 'created': new_issue.fields.created,
393
+ 'url': f"{self.jira._options['server']}/browse/{new_issue.key}"
394
+ }
395
+
396
+ return json.dumps(result, indent=2)
397
+
398
+ except Exception as e:
399
+ raise ValueError(f"Failed to create issue: {str(e)}")
400
+
401
+ async def transition_issue(self, issue_key: str, transition_name: str):
402
+ """Transition a Jira issue to a new status."""
403
+ try:
404
+ issue = self.jira.issue(issue_key)
405
+
406
+ # Get available transitions
407
+ transitions = self.jira.transitions(issue)
408
+ transition_id = None
409
+
410
+ for t in transitions:
411
+ if t['name'].lower() == transition_name.lower():
412
+ transition_id = t['id']
413
+ break
414
+
415
+ if not transition_id:
416
+ available = [t['name'] for t in transitions]
417
+ raise ValueError(f"Transition '{transition_name}' not found. Available: {', '.join(available)}")
418
+
419
+ # Perform transition
420
+ self.jira.transition_issue(issue, transition_id)
421
+
422
+ # Get updated issue
423
+ updated_issue = self.jira.issue(issue_key)
424
+
425
+ result = {
426
+ 'issue_key': issue_key,
427
+ 'summary': updated_issue.fields.summary,
428
+ 'old_status': issue.fields.status.name,
429
+ 'new_status': updated_issue.fields.status.name,
430
+ 'transition': transition_name
431
+ }
432
+
433
+ return json.dumps(result, indent=2)
434
+
435
+ except Exception as e:
436
+ raise ValueError(f"Failed to transition issue: {str(e)}")
437
+
438
+ async def assign_issue(self, issue_key: str, assignee: str):
439
+ """Assign a Jira issue to a user."""
440
+ try:
441
+ issue = self.jira.issue(issue_key)
442
+ old_assignee = issue.fields.assignee.displayName if issue.fields.assignee else 'Unassigned'
443
+
444
+ # Assign issue (use None for unassign, or username/email)
445
+ if assignee.lower() in ['none', 'unassigned', '']:
446
+ self.jira.assign_issue(issue, None)
447
+ new_assignee = 'Unassigned'
448
+ else:
449
+ self.jira.assign_issue(issue, assignee)
450
+ updated_issue = self.jira.issue(issue_key)
451
+ new_assignee = updated_issue.fields.assignee.displayName if updated_issue.fields.assignee else 'Unassigned'
452
+
453
+ result = {
454
+ 'issue_key': issue_key,
455
+ 'summary': issue.fields.summary,
456
+ 'old_assignee': old_assignee,
457
+ 'new_assignee': new_assignee
458
+ }
459
+
460
+ return json.dumps(result, indent=2)
461
+
462
+ except Exception as e:
463
+ raise ValueError(f"Failed to assign issue: {str(e)}")
464
+
465
+ class JiraContext:
466
+ def __init__(self, connector: JiraApiClient):
467
+ self.connector = connector
468
+
469
+ @asynccontextmanager
470
+ async def server_lifespan(server: FastMCP) -> AsyncIterator[JiraContext]:
471
+ """Manage application lifecycle for Jira Api Client."""
472
+ jira_url = os.getenv("JIRA_URL")
473
+ jira_email = os.getenv("JIRA_EMAIL")
474
+ jira_token = os.getenv("JIRA_TOKEN")
475
+ connector = JiraApiClient(jira_url, jira_email, jira_token)
476
+
477
+ try:
478
+ yield JiraContext(connector)
479
+ finally:
480
+ pass
481
+
482
+ mcp = FastMCP(name="JIRA", lifespan=server_lifespan)
483
+
484
+ @mcp.tool()
485
+ async def get_jira_ticket_info(issue_key: str, ctx: Context) -> str:
486
+ """Get detailed information about a JIRA issue."""
487
+ if not issue_key:
488
+ return "Error: Missing required parameter 'issue_key'"
489
+
490
+ connector = ctx.request_context.lifespan_context.connector
491
+ try:
492
+ return await connector.get_jira_ticket_info(issue_key)
493
+ except ValueError as e:
494
+ return f"Error: {str(e)}"
495
+
496
+ @mcp.tool()
497
+ async def get_jira_ticket_attachments(issue_key: str, ctx: Context) -> str:
498
+ """Get the attachments of a JIRA issue."""
499
+ if not issue_key:
500
+ return "Error: Missing required parameter 'issue_key'"
501
+
502
+ connector = ctx.request_context.lifespan_context.connector
503
+ try:
504
+ return await connector.get_jira_ticket_attachments(issue_key)
505
+ except ValueError as e:
506
+ return f"Error: {str(e)}"
507
+
508
+ @mcp.tool()
509
+ async def search_issues(jql: str, max_results: int = 50, fields: Optional[str] = None, ctx: Context = None) -> str:
510
+ """Search for Jira issues using JQL (Jira Query Language).
511
+
512
+ Args:
513
+ jql: JQL query string (e.g., 'project = PROJ AND status = "In Progress"')
514
+ max_results: Maximum number of results to return (default: 50)
515
+ fields: Comma-separated list of fields to return (default: summary,status,assignee,priority,created,updated,duedate,labels,components)
516
+
517
+ Examples:
518
+ - 'project = MYPROJ'
519
+ - 'assignee = currentUser() AND status != Done'
520
+ - 'updated >= -7d ORDER BY updated DESC'
521
+ - 'labels = urgent AND duedate < now()'
522
+ """
523
+ if not jql:
524
+ return "Error: Missing required parameter 'jql'"
525
+
526
+ connector = ctx.request_context.lifespan_context.connector
527
+ try:
528
+ return await connector.search_issues(jql, max_results, fields)
529
+ except ValueError as e:
530
+ return f"Error: {str(e)}"
531
+
532
+ @mcp.tool()
533
+ async def get_epic_details(epic_key: str, ctx: Context) -> str:
534
+ """Get detailed information about an epic including all child issues and progress metrics.
535
+
536
+ Args:
537
+ epic_key: The epic issue key (e.g., 'PROJ-123')
538
+
539
+ Returns:
540
+ Epic summary, status, description, child issues, completion percentage, and story points
541
+ """
542
+ if not epic_key:
543
+ return "Error: Missing required parameter 'epic_key'"
544
+
545
+ connector = ctx.request_context.lifespan_context.connector
546
+ try:
547
+ return await connector.get_epic_details(epic_key)
548
+ except ValueError as e:
549
+ return f"Error: {str(e)}"
550
+
551
+ @mcp.tool()
552
+ async def get_sprint_info(board_id: int, sprint_id: Optional[int] = None, ctx: Context = None) -> str:
553
+ """Get information about sprints on a Jira board.
554
+
555
+ Args:
556
+ board_id: The Jira board ID
557
+ sprint_id: Optional specific sprint ID. If not provided, returns all sprints for the board
558
+
559
+ Returns:
560
+ Sprint details including name, state, dates, and issues (if sprint_id provided)
561
+ or list of all sprints (if sprint_id not provided)
562
+ """
563
+ if not board_id:
564
+ return "Error: Missing required parameter 'board_id'"
565
+
566
+ connector = ctx.request_context.lifespan_context.connector
567
+ try:
568
+ return await connector.get_sprint_info(board_id, sprint_id)
569
+ except ValueError as e:
570
+ return f"Error: {str(e)}"
571
+
572
+
573
+ @mcp.tool()
574
+ async def get_issue_history(issue_key: str, ctx: Context) -> str:
575
+ """Get the change history of a Jira issue.
576
+
577
+ Args:
578
+ issue_key: The issue key (e.g., 'PROJ-123')
579
+
580
+ Returns:
581
+ Complete change history including status transitions, assignee changes, and field updates
582
+ """
583
+ if not issue_key:
584
+ return "Error: Missing required parameter 'issue_key'"
585
+
586
+ connector = ctx.request_context.lifespan_context.connector
587
+ try:
588
+ return await connector.get_issue_history(issue_key)
589
+ except ValueError as e:
590
+ return f"Error: {str(e)}"
591
+
592
+ @mcp.tool()
593
+ async def get_project_releases(project_key: str, ctx: Context) -> str:
594
+ """Get all releases/versions for a project.
595
+
596
+ Args:
597
+ project_key: The project key (e.g., 'PROJ')
598
+
599
+ Returns:
600
+ List of all releases with their status, dates, and associated issues
601
+ """
602
+ if not project_key:
603
+ return "Error: Missing required parameter 'project_key'"
604
+
605
+ connector = ctx.request_context.lifespan_context.connector
606
+ try:
607
+ return await connector.get_project_releases(project_key)
608
+ except ValueError as e:
609
+ return f"Error: {str(e)}"
610
+
611
+ @mcp.tool()
612
+ async def get_issue_links(issue_key: str, ctx: Context) -> str:
613
+ """Get all linked issues for a given issue.
614
+
615
+ Args:
616
+ issue_key: The issue key (e.g., 'PROJ-123')
617
+
618
+ Returns:
619
+ All issue links including blocks/blocked by, relates to, and other link types
620
+ """
621
+ if not issue_key:
622
+ return "Error: Missing required parameter 'issue_key'"
623
+
624
+ connector = ctx.request_context.lifespan_context.connector
625
+ try:
626
+ return await connector.get_issue_links(issue_key)
627
+ except ValueError as e:
628
+ return f"Error: {str(e)}"
629
+
630
+ @mcp.tool()
631
+ async def add_comment(issue_key: str, comment_text: str, ctx: Context) -> str:
632
+ """Add a comment to a Jira issue.
633
+
634
+ Args:
635
+ issue_key: The issue key (e.g., 'PROJ-123')
636
+ comment_text: The comment text to add
637
+
638
+ Returns:
639
+ Comment details including ID, author, and timestamp
640
+ """
641
+ if not issue_key or not comment_text:
642
+ return "Error: Missing required parameters 'issue_key' and 'comment_text'"
643
+
644
+ connector = ctx.request_context.lifespan_context.connector
645
+ try:
646
+ return await connector.add_comment(issue_key, comment_text)
647
+ except ValueError as e:
648
+ return f"Error: {str(e)}"
649
+
650
+ @mcp.tool()
651
+ async def update_issue(issue_key: str, fields: dict, ctx: Context) -> str:
652
+ """Update fields of a Jira issue.
653
+
654
+ Args:
655
+ issue_key: The issue key (e.g., 'PROJ-123')
656
+ fields: Dictionary of fields to update (e.g., {'summary': 'New title', 'description': 'New desc'})
657
+
658
+ Returns:
659
+ Updated issue details
660
+
661
+ Examples:
662
+ - Update summary: {'summary': 'New title'}
663
+ - Update description: {'description': 'New description'}
664
+ - Update priority: {'priority': {'name': 'High'}}
665
+ - Update labels: {'labels': ['bug', 'urgent']}
666
+ """
667
+ if not issue_key or not fields:
668
+ return "Error: Missing required parameters 'issue_key' and 'fields'"
669
+
670
+ connector = ctx.request_context.lifespan_context.connector
671
+ try:
672
+ return await connector.update_issue(issue_key, fields)
673
+ except ValueError as e:
674
+ return f"Error: {str(e)}"
675
+
676
+ @mcp.tool()
677
+ async def create_issue(project_key: str, summary: str, description: str, issue_type: str = "Task", ctx: Context = None) -> str:
678
+ """Create a new Jira issue.
679
+
680
+ Args:
681
+ project_key: The project key (e.g., 'PROJ')
682
+ summary: Issue title/summary
683
+ description: Issue description
684
+ issue_type: Type of issue (default: 'Task', options: 'Bug', 'Story', 'Epic', etc.)
685
+
686
+ Returns:
687
+ Created issue details including key and URL
688
+ """
689
+ if not project_key or not summary or not description:
690
+ return "Error: Missing required parameters 'project_key', 'summary', and 'description'"
691
+
692
+ connector = ctx.request_context.lifespan_context.connector
693
+ try:
694
+ return await connector.create_issue(project_key, summary, description, issue_type)
695
+ except ValueError as e:
696
+ return f"Error: {str(e)}"
697
+
698
+ @mcp.tool()
699
+ async def transition_issue(issue_key: str, transition_name: str, ctx: Context) -> str:
700
+ """Transition a Jira issue to a new status.
701
+
702
+ Args:
703
+ issue_key: The issue key (e.g., 'PROJ-123')
704
+ transition_name: Name of the transition (e.g., 'In Progress', 'Done', 'To Do')
705
+
706
+ Returns:
707
+ Transition details including old and new status
708
+
709
+ Note: Available transitions depend on the workflow. If transition fails,
710
+ the error will list available transitions for the issue.
711
+ """
712
+ if not issue_key or not transition_name:
713
+ return "Error: Missing required parameters 'issue_key' and 'transition_name'"
714
+
715
+ connector = ctx.request_context.lifespan_context.connector
716
+ try:
717
+ return await connector.transition_issue(issue_key, transition_name)
718
+ except ValueError as e:
719
+ return f"Error: {str(e)}"
720
+
721
+ @mcp.tool()
722
+ async def assign_issue(issue_key: str, assignee: str, ctx: Context) -> str:
723
+ """Assign a Jira issue to a user.
724
+
725
+ Args:
726
+ issue_key: The issue key (e.g., 'PROJ-123')
727
+ assignee: Username or email of assignee (use 'none' or 'unassigned' to unassign)
728
+
729
+ Returns:
730
+ Assignment details including old and new assignee
731
+ """
732
+ if not issue_key or not assignee:
733
+ return "Error: Missing required parameters 'issue_key' and 'assignee'"
734
+
735
+ connector = ctx.request_context.lifespan_context.connector
736
+ try:
737
+ return await connector.assign_issue(issue_key, assignee)
738
+ except ValueError as e:
739
+ return f"Error: {str(e)}"
740
+
741
+ def main():
742
+ mcp.run()
743
+
744
+ if __name__ == "__main__":
745
+ main()