#
tokens: 23922/50000 2/48 files (page 2/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 4. Use http://codebase.md/infinitiq-tech/mcp-jira?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .env.example
├── .github
│   ├── copilot-instructions.md
│   └── ISSUE_TEMPLATE
│       ├── bug_report.yml
│       ├── config.yml
│       ├── copilot.yml
│       ├── story.yml
│       └── task.yml
├── .gitignore
├── .lock
├── bin
│   ├── activate
│   ├── activate.csh
│   ├── activate.fish
│   ├── Activate.ps1
│   ├── dotenv
│   ├── httpx
│   ├── jirashell
│   ├── mcp
│   ├── mcp-server-jira
│   ├── normalizer
│   ├── pip
│   ├── pip3
│   ├── pip3.10
│   ├── python
│   ├── python3
│   ├── python3.10
│   └── uvicorn
├── claude_reference
│   ├── jira_advanced.txt
│   ├── jira_api_documentation.txt
│   ├── jira_examples.txt
│   ├── jira_installation.md
│   └── jira_shell.txt
├── Dockerfile
├── docs
│   └── create_issues_v3_conversion.md
├── glama.json
├── jira.py
├── LICENSE
├── mcp
│   ├── __init__.py
│   ├── server
│   │   └── __init__.py
│   └── types
│       └── __init__.py
├── MCPReadme.md
├── pyproject.toml
├── pytest.ini
├── pyvenv.cfg
├── README.md
├── run_server.py
├── src
│   ├── __init__.py
│   ├── mcp
│   │   ├── __init__.py
│   │   ├── server
│   │   │   ├── __init__.py
│   │   │   └── stdio.py
│   │   └── types
│   │       └── __init__.py
│   └── mcp_server_jira
│       ├── __init__.py
│       ├── __main__.py
│       ├── jira_v3_api.py
│       └── server.py
├── tests
│   ├── __init__.py
│   ├── test_add_comment_v3_api_only.py
│   ├── test_bulk_create_issues_v3_api.py
│   ├── test_create_issue_v3_api_only.py
│   ├── test_create_issues_integration.py
│   ├── test_create_jira_issues_server.py
│   ├── test_get_transitions_v3.py
│   ├── test_jira_v3_api.py
│   ├── test_search_issues_v3_api.py
│   ├── test_server.py
│   └── test_transition_issue_v3_api_only.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/src/mcp_server_jira/jira_v3_api.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Jira v3 REST API client module
  3 | 
  4 | This module provides direct HTTP client functionality for Jira's v3 REST API,
  5 | offering enhanced functionality and security for operations that require the latest API features.
  6 | """
  7 | 
  8 | import json
  9 | import logging
 10 | from typing import Any, Dict, Optional
 11 | 
 12 | import httpx
 13 | 
 14 | logger = logging.getLogger("JiraMCPLogger")  # Get the same logger instance
 15 | 
 16 | 
 17 | class JiraV3APIClient:
 18 |     """Client for making direct requests to Jira's v3 REST API"""
 19 | 
 20 |     def __init__(
 21 |         self,
 22 |         server_url: str,
 23 |         username: Optional[str] = None,
 24 |         password: Optional[str] = None,
 25 |         token: Optional[str] = None,
 26 |     ):
 27 |         """Initialize the v3 API client
 28 | 
 29 |         Args:
 30 |             server_url: Jira server URL
 31 |             username: Username for authentication
 32 |             password: Password for basic auth
 33 |             token: API token for auth
 34 |         """
 35 |         self.server_url = server_url.rstrip("/")
 36 |         self.username = username
 37 |         self.auth_token = token or password
 38 | 
 39 |         if not self.username or not self.auth_token:
 40 |             raise ValueError(
 41 |                 "Jira username and an API token (or password) are required for v3 API."
 42 |             )
 43 | 
 44 |         self.client = httpx.AsyncClient(
 45 |             auth=(self.username, self.auth_token),
 46 |             headers={"Accept": "application/json", "Content-Type": "application/json"},
 47 |             timeout=30.0,
 48 |             follow_redirects=True,
 49 |         )
 50 | 
 51 |     async def _make_v3_api_request(
 52 |         self,
 53 |         method: str,
 54 |         endpoint: str,
 55 |         data: Optional[Dict[str, Any]] = None,
 56 |         params: Optional[Dict[str, Any]] = None,
 57 |     ) -> Dict[str, Any]:
 58 |         """
 59 |         Sends an authenticated async HTTP request to a Jira v3 REST API endpoint.
 60 |         """
 61 |         url = f"{self.server_url}/rest/api/3{endpoint}"
 62 | 
 63 |         logger.debug(f"Attempting to make request: {method} {url}")
 64 |         logger.debug(f"Request params: {params}")
 65 |         logger.debug(f"Request JSON data: {data}")
 66 | 
 67 |         try:
 68 |             logger.info(f"AWAITING httpx.client.request for {method} {url}")
 69 |             response = await self.client.request(
 70 |                 method=method.upper(),
 71 |                 url=url,
 72 |                 json=data,
 73 |                 params=params,
 74 |             )
 75 |             logger.info(
 76 |                 f"COMPLETED httpx.client.request for {url}. Status: {response.status_code}"
 77 |             )
 78 |             logger.debug(f"Raw response text (first 500 chars): {str(response.text)[:500]}")
 79 | 
 80 |             response.raise_for_status()
 81 | 
 82 |             if response.status_code == 204:
 83 |                 return {}
 84 | 
 85 |             return response.json()
 86 | 
 87 |         except httpx.HTTPStatusError as e:
 88 |             logger.error(
 89 |                 f"HTTP Status Error for {e.request.url!r}: {e.response.status_code}",
 90 |                 exc_info=True,
 91 |             )
 92 |             error_details = f"Jira API returned an error: {e.response.status_code} {e.response.reason_phrase}."
 93 |             raise ValueError(error_details)
 94 | 
 95 |         except httpx.RequestError as e:
 96 |             logger.error(f"Request Error for {e.request.url!r}", exc_info=True)
 97 |             raise ValueError(f"A network error occurred while connecting to Jira: {e}")
 98 |         except Exception as e:
 99 |             logger.critical(
100 |                 "An unexpected error occurred in _make_v3_api_request", exc_info=True
101 |             )
102 |             raise
103 | 
104 |     async def create_project(
105 |         self,
106 |         key: str,
107 |         assignee: str,
108 |         name: Optional[str] = None,
109 |         ptype: str = None,
110 |         template_name: Optional[str] = None,
111 |         avatarId: Optional[int] = None,
112 |         issueSecurityScheme: Optional[int] = None,
113 |         permissionScheme: Optional[int] = None,
114 |         projectCategory: Optional[int] = None,
115 |         notificationScheme: Optional[int] = None,
116 |         categoryId: Optional[int] = None,
117 |         url: str = None,
118 |     ) -> Dict[str, Any]:
119 |         """
120 |         Creates a new Jira project using the v3 REST API.
121 | 
122 |         Requires a project key and the Atlassian accountId of the project lead (`assignee`). The v3 API mandates that `leadAccountId` is always provided, regardless of default project lead settings or UI behavior. Additional project attributes such as name, type, template, avatar, schemes, category, and documentation URL can be specified.
123 | 
124 |         Args:
125 |             key: The unique project key (required).
126 |             name: The project name. Defaults to the key if not provided.
127 |             assignee: Atlassian accountId of the project lead (required by v3 API).
128 |             ptype: Project type key (e.g., 'software', 'business', 'service_desk').
129 |             template_name: Project template key for template-based creation.
130 |             avatarId: ID of the avatar to assign to the project.
131 |             issueSecurityScheme: ID of the issue security scheme.
132 |             permissionScheme: ID of the permission scheme.
133 |             projectCategory: ID of the project category.
134 |             notificationScheme: ID of the notification scheme.
135 |             categoryId: Alternative to projectCategory; preferred for v3 API.
136 |             url: URL for project information or documentation.
137 | 
138 |         Returns:
139 |             A dictionary containing details of the created project as returned by Jira.
140 | 
141 |         Raises:
142 |             ValueError: If required parameters are missing or project creation fails.
143 |         """
144 |         if not key:
145 |             raise ValueError("Project key is required")
146 | 
147 |         if not assignee:
148 |             raise ValueError(
149 |                 "Parameter 'assignee' (leadAccountId) is required by the Jira v3 API"
150 |             )
151 | 
152 |         payload = {
153 |             "key": key,
154 |             "name": name or key,
155 |             "leadAccountId": assignee,
156 |             "assigneeType": "PROJECT_LEAD",
157 |             "projectTypeKey": ptype,
158 |             "projectTemplateKey": template_name,
159 |             "avatarId": avatarId,
160 |             "issueSecurityScheme": issueSecurityScheme,
161 |             "permissionScheme": permissionScheme,
162 |             "notificationScheme": notificationScheme,
163 |             "categoryId": categoryId or projectCategory,
164 |             "url": url,
165 |         }
166 | 
167 |         payload = {k: v for k, v in payload.items() if v is not None}
168 | 
169 |         print(f"Creating project with v3 API payload: {json.dumps(payload, indent=2)}")
170 |         response_data = await self._make_v3_api_request(
171 |             "POST", "/project", data=payload
172 |         )
173 |         print(f"Project creation response: {json.dumps(response_data, indent=2)}")
174 |         return response_data
175 | 
176 |     async def get_projects(
177 |         self,
178 |         start_at: int = 0,
179 |         max_results: int = 50,
180 |         order_by: Optional[str] = None,
181 |         ids: Optional[list] = None,
182 |         keys: Optional[list] = None,
183 |         query: Optional[str] = None,
184 |         type_key: Optional[str] = None,
185 |         category_id: Optional[int] = None,
186 |         action: Optional[str] = None,
187 |         expand: Optional[str] = None,
188 |     ) -> Dict[str, Any]:
189 |         """
190 |         Get projects paginated using the v3 REST API.
191 | 
192 |         Returns a paginated list of projects visible to the user using the
193 |         /rest/api/3/project/search endpoint.
194 | 
195 |         Args:
196 |             start_at: The index of the first item to return (default: 0)
197 |             max_results: The maximum number of items to return per page (default: 50)
198 |             order_by: Order the results by a field:
199 |                      - category: Order by project category
200 |                      - issueCount: Order by total number of issues
201 |                      - key: Order by project key
202 |                      - lastIssueUpdatedDate: Order by last issue update date
203 |                      - name: Order by project name
204 |                      - owner: Order by project lead
205 |                      - archivedDate: Order by archived date
206 |                      - deletedDate: Order by deleted date
207 |             ids: List of project IDs to return
208 |             keys: List of project keys to return
209 |             query: Filter projects by query string
210 |             type_key: Filter projects by type key
211 |             category_id: Filter projects by category ID
212 |             action: Filter by action permission (view, browse, edit)
213 |             expand: Expand additional project fields in response
214 | 
215 |         Returns:
216 |             Dictionary containing the paginated response with projects and pagination info
217 | 
218 |         Raises:
219 |             ValueError: If the API request fails
220 |         """
221 |         params = {
222 |             "startAt": start_at,
223 |             "maxResults": max_results,
224 |             "orderBy": order_by,
225 |             "id": ids,
226 |             "keys": keys,
227 |             "query": query,
228 |             "typeKey": type_key,
229 |             "categoryId": category_id,
230 |             "action": action,
231 |             "expand": expand,
232 |         }
233 | 
234 |         params = {k: v for k, v in params.items() if v is not None}
235 | 
236 |         endpoint = "/project/search"
237 |         print(
238 |             f"Fetching projects with v3 API endpoint: {endpoint} with params: {params}"
239 |         )
240 |         response_data = await self._make_v3_api_request("GET", endpoint, params=params)
241 |         print(f"Projects API response: {json.dumps(response_data, indent=2)}")
242 |         return response_data
243 | 
244 |     async def get_transitions(
245 |         self,
246 |         issue_id_or_key: str,
247 |         expand: Optional[str] = None,
248 |         transition_id: Optional[str] = None,
249 |         skip_remote_only_condition: Optional[bool] = None,
250 |         include_unavailable_transitions: Optional[bool] = None,
251 |         sort_by_ops_bar_and_status: Optional[bool] = None,
252 |     ) -> Dict[str, Any]:
253 |         """
254 |         Get available transitions for an issue using the v3 REST API.
255 | 
256 |         Returns either all transitions or a transition that can be performed by the user
257 |         on an issue, based on the issue's status.
258 | 
259 |         Args:
260 |             issue_id_or_key: Issue ID or key (required)
261 |             expand: Expand additional transition fields in response
262 |             transition_id: Get only the transition matching this ID
263 |             skip_remote_only_condition: Skip remote-only conditions check
264 |             include_unavailable_transitions: Include transitions that can't be performed
265 |             sort_by_ops_bar_and_status: Sort transitions by operations bar and status
266 | 
267 |         Returns:
268 |             Dictionary containing the transitions response with transition details
269 | 
270 |         Raises:
271 |             ValueError: If the API request fails
272 |         """
273 |         if not issue_id_or_key:
274 |             raise ValueError("issue_id_or_key is required")
275 | 
276 |         params = {
277 |             "expand": expand,
278 |             "transitionId": transition_id,
279 |             "skipRemoteOnlyCondition": skip_remote_only_condition,
280 |             "includeUnavailableTransitions": include_unavailable_transitions,
281 |             "sortByOpsBarAndStatus": sort_by_ops_bar_and_status,
282 |         }
283 | 
284 |         params = {k: v for k, v in params.items() if v is not None}
285 | 
286 |         endpoint = f"/issue/{issue_id_or_key}/transitions"
287 |         logger.debug(
288 |             f"Fetching transitions with v3 API endpoint: {endpoint} with params: {params}"
289 |         )
290 |         response_data = await self._make_v3_api_request("GET", endpoint, params=params)
291 |         logger.debug(f"Transitions API response: {json.dumps(response_data, indent=2)}")
292 |         return response_data
293 | 
294 |     async def transition_issue(
295 |         self,
296 |         issue_id_or_key: str,
297 |         transition_id: str,
298 |         fields: Optional[Dict[str, Any]] = None,
299 |         comment: Optional[str] = None,
300 |         history_metadata: Optional[Dict[str, Any]] = None,
301 |         properties: Optional[list] = None,
302 |     ) -> Dict[str, Any]:
303 |         """
304 |         Transition an issue using the v3 REST API.
305 | 
306 |         Performs an issue transition and, if the transition has a screen,
307 |         updates the fields from the transition screen.
308 | 
309 |         Args:
310 |             issue_id_or_key: Issue ID or key (required)
311 |             transition_id: ID of the transition to perform (required)
312 |             fields: Dict containing field names and values to update during transition
313 |             comment: Simple string comment to add during transition
314 |             history_metadata: Optional history metadata for the transition
315 |             properties: Optional list of properties to set
316 | 
317 |         Returns:
318 |             Empty dictionary on success (204 No Content response)
319 | 
320 |         Raises:
321 |             ValueError: If required parameters are missing or transition fails
322 |         """
323 |         if not issue_id_or_key:
324 |             raise ValueError("issue_id_or_key is required")
325 | 
326 |         if not transition_id:
327 |             raise ValueError("transition_id is required")
328 | 
329 |         # Build the request payload
330 |         payload = {"transition": {"id": transition_id}}
331 | 
332 |         # Add fields if provided
333 |         if fields:
334 |             payload["fields"] = fields
335 | 
336 |         # Add comment if provided - convert simple string to ADF format
337 |         if comment:
338 |             payload["update"] = {
339 |                 "comment": [
340 |                     {
341 |                         "add": {
342 |                             "body": {
343 |                                 "type": "doc",
344 |                                 "version": 1,
345 |                                 "content": [
346 |                                     {
347 |                                         "type": "paragraph",
348 |                                         "content": [{"type": "text", "text": comment}],
349 |                                     }
350 |                                 ],
351 |                             }
352 |                         }
353 |                     }
354 |                 ]
355 |             }
356 | 
357 |         # Add optional metadata
358 |         if history_metadata:
359 |             payload["historyMetadata"] = history_metadata
360 | 
361 |         if properties:
362 |             payload["properties"] = properties
363 | 
364 |         endpoint = f"/issue/{issue_id_or_key}/transitions"
365 |         logger.debug(f"Transitioning issue with v3 API endpoint: {endpoint}")
366 |         logger.debug(f"Transition payload: {json.dumps(payload, indent=2)}")
367 | 
368 |         response_data = await self._make_v3_api_request("POST", endpoint, data=payload)
369 |         logger.debug(f"Transition response: {response_data}")
370 |         return response_data
371 | 
372 |     async def get_issue_types(self) -> Dict[str, Any]:
373 |         """
374 |         Get all issue types for user using the v3 REST API.
375 | 
376 |         Returns all issue types. This operation can be accessed anonymously.
377 | 
378 |         Permissions required: Issue types are only returned as follows:
379 |         - if the user has the Administer Jira global permission, all issue types are returned.
380 |         - if the user has the Browse projects project permission for one or more projects,
381 |           the issue types associated with the projects the user has permission to browse are returned.
382 |         - if the user is anonymous then they will be able to access projects with the Browse projects for anonymous users
383 |         - if the user authentication is incorrect they will fall back to anonymous
384 | 
385 |         Returns:
386 |             List of issue type dictionaries with fields like:
387 |             - avatarId: Avatar ID for the issue type
388 |             - description: Description of the issue type
389 |             - hierarchyLevel: Hierarchy level
390 |             - iconUrl: URL of the issue type icon
391 |             - id: Issue type ID
392 |             - name: Issue type name
393 |             - self: REST API URL for the issue type
394 |             - subtask: Whether this is a subtask type
395 | 
396 |         Raises:
397 |             ValueError: If the API request fails
398 |         """
399 |         endpoint = "/issuetype"
400 |         logger.debug(f"Fetching issue types with v3 API endpoint: {endpoint}")
401 |         response_data = await self._make_v3_api_request("GET", endpoint)
402 |         logger.debug(f"Issue types API response: {json.dumps(response_data, indent=2)}")
403 |         return response_data
404 | 
405 |     async def add_comment(
406 |         self,
407 |         issue_id_or_key: str,
408 |         comment: str,
409 |         visibility: Optional[Dict[str, str]] = None,
410 |         properties: Optional[list] = None,
411 |     ) -> Dict[str, Any]:
412 |         """
413 |         Add a comment to an issue using the v3 REST API.
414 | 
415 |         Args:
416 |             issue_id_or_key: Issue ID or key (required)
417 |             comment: Comment text to add (required)
418 |             visibility: Optional visibility settings (e.g., {"type": "role", "value": "Administrators"})
419 |             properties: Optional list of properties to set
420 | 
421 |         Returns:
422 |             Dict containing comment details:
423 |             - id: Comment ID
424 |             - body: Comment body in ADF format
425 |             - author: Author information
426 |             - created: Creation timestamp
427 |             - updated: Last update timestamp
428 |             - etc.
429 | 
430 |         Raises:
431 |             ValueError: If required parameters are missing or comment creation fails
432 |         """
433 |         if not issue_id_or_key:
434 |             raise ValueError("issue_id_or_key is required")
435 | 
436 |         if not comment:
437 |             raise ValueError("comment is required")
438 | 
439 |         # Build the request payload with ADF format
440 |         payload = {
441 |             "body": {
442 |                 "type": "doc",
443 |                 "version": 1,
444 |                 "content": [
445 |                     {
446 |                         "type": "paragraph",
447 |                         "content": [{"type": "text", "text": comment}],
448 |                     }
449 |                 ],
450 |             }
451 |         }
452 | 
453 |         # Add optional visibility
454 |         if visibility:
455 |             payload["visibility"] = visibility
456 | 
457 |         # Add optional properties
458 |         if properties:
459 |             payload["properties"] = properties
460 | 
461 |         endpoint = f"/issue/{issue_id_or_key}/comment"
462 |         logger.debug(f"Adding comment to issue {issue_id_or_key} with v3 API endpoint: {endpoint}")
463 |         response_data = await self._make_v3_api_request("POST", endpoint, data=payload)
464 |         logger.debug(f"Add comment API response: {json.dumps(response_data, indent=2)}")
465 |         return response_data
466 | 
467 |     async def create_issue(
468 |         self,
469 |         fields: Dict[str, Any],
470 |         update: Optional[Dict[str, Any]] = None,
471 |         history_metadata: Optional[Dict[str, Any]] = None,
472 |         properties: Optional[list] = None,
473 |         transition: Optional[Dict[str, Any]] = None,
474 |     ) -> Dict[str, Any]:
475 |         """
476 |         Create an issue using the v3 REST API.
477 | 
478 |         Creates an issue or, where the option to create subtasks is enabled in Jira, a subtask.
479 |         A transition may be applied, to move the issue or subtask to a workflow step other than
480 |         the default start step, and issue properties set.
481 | 
482 |         Args:
483 |             fields: Dict containing field names and values (required).
484 |                    Must include project, summary, description, and issuetype.
485 |             update: Dict containing update operations for fields
486 |             history_metadata: Optional history metadata for the issue creation
487 |             properties: Optional list of properties to set
488 |             transition: Optional transition to apply after creation
489 | 
490 |         Returns:
491 |             Dictionary containing the created issue details:
492 |             - id: Issue ID
493 |             - key: Issue key
494 |             - self: URL to the created issue
495 |             - transition: Transition result if applied
496 | 
497 |         Raises:
498 |             ValueError: If required parameters are missing or creation fails
499 |         """
500 |         if not fields:
501 |             raise ValueError("fields is required")
502 | 
503 |         # Build the request payload
504 |         payload = {"fields": fields}
505 | 
506 |         # Add optional parameters
507 |         if update:
508 |             payload["update"] = update
509 | 
510 |         if history_metadata:
511 |             payload["historyMetadata"] = history_metadata
512 | 
513 |         if properties:
514 |             payload["properties"] = properties
515 | 
516 |         if transition:
517 |             payload["transition"] = transition
518 | 
519 |         endpoint = "/issue"
520 |         logger.debug(f"Creating issue with v3 API endpoint: {endpoint}")
521 |         logger.debug(f"Create issue payload: {json.dumps(payload, indent=2)}")
522 | 
523 |         response_data = await self._make_v3_api_request("POST", endpoint, data=payload)
524 |         logger.debug(f"Create issue response: {response_data}")
525 |         return response_data
526 | 
527 |     async def search_issues(
528 |         self,
529 |         jql: str,
530 |         start_at: int = 0,
531 |         max_results: int = 50,
532 |         fields: Optional[str] = None,
533 |         expand: Optional[str] = None,
534 |         properties: Optional[list] = None,
535 |         fields_by_keys: Optional[bool] = None,
536 |         fail_fast: Optional[bool] = None,
537 |         reconcile_issues: Optional[list] = None,
538 |     ) -> Dict[str, Any]:
539 |         """
540 |         Search for issues using JQL enhanced search (GET) via v3 REST API.
541 |         
542 |         Searches for issues using JQL. Recent updates might not be immediately visible 
543 |         in the returned search results. If you need read-after-write consistency, 
544 |         you can utilize the reconcileIssues parameter to ensure stronger consistency assurances. 
545 |         This operation can be accessed anonymously.
546 | 
547 |         Args:
548 |             jql: JQL query string
549 |             start_at: Index of the first issue to return (default: 0)
550 |             max_results: Maximum number of results to return (default: 50)
551 |             fields: Comma-separated list of fields to include in response
552 |             expand: Use expand to include additional information about issues
553 |             properties: List of issue properties to include in response
554 |             fields_by_keys: Reference fields by their key (rather than ID)
555 |             fail_fast: Fail fast when JQL query validation fails
556 |             reconcile_issues: List of issue IDs to reconcile for read-after-write consistency
557 | 
558 |         Returns:
559 |             Dictionary containing search results with:
560 |             - issues: List of issue dictionaries
561 |             - isLast: Boolean indicating if this is the last page
562 |             - startAt: Starting index of results
563 |             - maxResults: Maximum results per page
564 |             - total: Total number of issues matching the query
565 | 
566 |         Raises:
567 |             ValueError: If the API request fails or JQL is invalid
568 |         """
569 |         if not jql:
570 |             raise ValueError("jql parameter is required")
571 | 
572 |         # Build query parameters
573 |         params = {
574 |             "jql": jql,
575 |             "startAt": start_at,
576 |             "maxResults": max_results,
577 |         }
578 | 
579 |         # Add optional parameters if provided
580 |         params["fields"] = fields if fields is not None else "*all"
581 |         if expand:
582 |             params["expand"] = expand
583 |         if properties:
584 |             params["properties"] = properties
585 |         if fields_by_keys is not None:
586 |             params["fieldsByKeys"] = fields_by_keys
587 |         if fail_fast is not None:
588 |             params["failFast"] = fail_fast
589 |         if reconcile_issues:
590 |             params["reconcileIssues"] = reconcile_issues
591 | 
592 |         # Remove None values
593 |         params = {k: v for k, v in params.items() if v is not None}
594 | 
595 |         endpoint = "/search/jql"
596 |         logger.debug(f"Searching issues with v3 API endpoint: {endpoint}")
597 |         logger.debug(f"Search params: {params}")
598 |         
599 |         response_data = await self._make_v3_api_request("GET", endpoint, params=params)
600 |         logger.debug(f"Search issues API response: {json.dumps(response_data, indent=2)}")
601 |         return response_data
602 | 
603 |     async def bulk_create_issues(
604 |         self, 
605 |         issue_updates: list
606 |     ) -> Dict[str, Any]:
607 |         """
608 |         Bulk create issues using the v3 REST API.
609 | 
610 |         Creates up to 50 issues and, where the option to create subtasks is enabled in Jira,
611 |         subtasks. Transitions may be applied, to move the issues or subtasks to a workflow
612 |         step other than the default start step, and issue properties set.
613 | 
614 |         Args:
615 |             issue_updates: List of issue creation specifications. Each item should contain
616 |                           'fields' dict with issue fields, and optionally 'update' dict
617 |                           for additional operations during creation.
618 | 
619 |         Returns:
620 |             Dict containing:
621 |             - issues: List of successfully created issues with their details
622 |             - errors: List of errors for failed issue creations
623 | 
624 |         Raises:
625 |             ValueError: If required parameters are missing or bulk creation fails
626 |         """
627 |         if not issue_updates:
628 |             raise ValueError("issue_updates list cannot be empty")
629 | 
630 |         if len(issue_updates) > 50:
631 |             raise ValueError("Cannot create more than 50 issues in a single bulk operation")
632 | 
633 |         # Build the request payload for v3 API
634 |         payload = {"issueUpdates": issue_updates}
635 | 
636 |         endpoint = "/issue/bulk"
637 |         logger.debug(f"Bulk creating issues with v3 API endpoint: {endpoint}")
638 |         logger.debug(f"Payload: {json.dumps(payload, indent=2)}")
639 | 
640 |         response_data = await self._make_v3_api_request("POST", endpoint, data=payload)
641 |         logger.debug(f"Bulk create response: {json.dumps(response_data, indent=2)}")
642 | 
643 |         return response_data
644 | 
```

--------------------------------------------------------------------------------
/src/mcp_server_jira/server.py:
--------------------------------------------------------------------------------

```python
   1 | import asyncio
   2 | import json
   3 | import logging
   4 | import os
   5 | import sys
   6 | from enum import Enum
   7 | from pathlib import Path
   8 | from typing import Any, Dict, List, Optional, Sequence, Union
   9 | 
  10 | # --- Setup a dedicated file logger ---
  11 | log_file_path = Path(__file__).parent / "jira_mcp_debug.log"
  12 | logger = logging.getLogger("JiraMCPLogger")
  13 | logger.setLevel(logging.DEBUG)  # Capture all levels of logs
  14 | 
  15 | # Create a file handler to write logs to a file
  16 | # Use 'w' to overwrite the file on each run, ensuring a clean log
  17 | handler = logging.FileHandler(log_file_path, mode="w")
  18 | handler.setLevel(logging.DEBUG)
  19 | 
  20 | # Create a formatter to make the logs readable
  21 | formatter = logging.Formatter(
  22 |     "%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s"
  23 | )
  24 | handler.setFormatter(formatter)
  25 | 
  26 | # Add the handler to the logger
  27 | if not logger.handlers:
  28 |     logger.addHandler(handler)
  29 | 
  30 | logger.info("Logger initialized. All subsequent logs will go to jira_mcp_debug.log")
  31 | # --- End of logger setup ---
  32 | 
  33 | try:
  34 |     from jira import JIRA
  35 | except ImportError:
  36 |     from .jira import JIRA
  37 | 
  38 | from pydantic import BaseModel
  39 | 
  40 | from mcp.server import Server
  41 | from mcp.server.stdio import stdio_server
  42 | from mcp.types import EmbeddedResource, ImageContent, TextContent, Tool
  43 | 
  44 | from .jira_v3_api import JiraV3APIClient
  45 | 
  46 | try:
  47 |     from dotenv import load_dotenv
  48 | 
  49 |     # Try to load from .env file if it exists
  50 |     env_path = Path(__file__).parent.parent.parent.parent / ".env"
  51 |     if env_path.exists():
  52 |         load_dotenv(dotenv_path=env_path)
  53 | except ImportError:
  54 |     # dotenv is optional
  55 |     pass
  56 | 
  57 | 
  58 | class JiraTools(str, Enum):
  59 |     GET_PROJECTS = "get_jira_projects"
  60 |     GET_ISSUE = "get_jira_issue"
  61 |     SEARCH_ISSUES = "search_jira_issues"
  62 |     CREATE_ISSUE = "create_jira_issue"
  63 |     CREATE_ISSUES = "create_jira_issues"
  64 |     ADD_COMMENT = "add_jira_comment"
  65 |     GET_TRANSITIONS = "get_jira_transitions"
  66 |     TRANSITION_ISSUE = "transition_jira_issue"
  67 |     CREATE_PROJECT = "create_jira_project"
  68 |     GET_PROJECT_ISSUE_TYPES = "get_jira_project_issue_types"
  69 | 
  70 | 
  71 | class JiraIssueField(BaseModel):
  72 |     name: str
  73 |     value: str
  74 | 
  75 | 
  76 | class JiraIssueResult(BaseModel):
  77 |     key: str
  78 |     summary: str
  79 |     description: Optional[str] = None
  80 |     status: Optional[str] = None
  81 |     assignee: Optional[str] = None
  82 |     reporter: Optional[str] = None
  83 |     created: Optional[str] = None
  84 |     updated: Optional[str] = None
  85 |     fields: Optional[Dict[str, Any]] = None
  86 |     comments: Optional[List[Dict[str, Any]]] = None
  87 |     watchers: Optional[Dict[str, Any]] = None
  88 |     attachments: Optional[List[Dict[str, Any]]] = None
  89 |     subtasks: Optional[List[Dict[str, Any]]] = None
  90 |     project: Optional[Dict[str, Any]] = None
  91 |     issue_links: Optional[List[Dict[str, Any]]] = None
  92 |     worklog: Optional[List[Dict[str, Any]]] = None
  93 |     timetracking: Optional[Dict[str, Any]] = None
  94 | 
  95 | 
  96 | class JiraProjectResult(BaseModel):
  97 |     key: str
  98 |     name: str
  99 |     id: str
 100 |     lead: Optional[str] = None
 101 | 
 102 | 
 103 | class JiraTransitionResult(BaseModel):
 104 |     id: str
 105 |     name: str
 106 | 
 107 | 
 108 | class JiraServer:
 109 |     def __init__(
 110 |         self,
 111 |         server_url: str = None,
 112 |         auth_method: str = None,
 113 |         username: str = None,
 114 |         password: str = None,
 115 |         token: str = None,
 116 |     ):
 117 |         self.server_url = server_url
 118 |         self.auth_method = auth_method
 119 |         self.username = username
 120 |         self.password = password
 121 |         self.token = token
 122 | 
 123 |         self._v3_api_client = JiraV3APIClient(
 124 |             server_url=self.server_url,
 125 |             username=self.username,
 126 |             token=self.token,
 127 |             password=password,
 128 |         )
 129 |         self.client = None
 130 | 
 131 |     def connect(self):
 132 |         """Connect to Jira server using provided authentication details"""
 133 |         if not self.server_url:
 134 |             print("Error: Jira server URL not provided")
 135 |             return False
 136 | 
 137 |         error_messages = []
 138 | 
 139 |         # Try multiple auth methods if possible
 140 |         try:
 141 |             # First, try the specified auth method
 142 |             if self.auth_method == "basic_auth":
 143 |                 # Basic auth - either username/password or username/token
 144 |                 if self.username and self.password:
 145 |                     try:
 146 |                         print(f"Trying basic_auth with username and password")
 147 |                         self.client = JIRA(
 148 |                             server=self.server_url,
 149 |                             basic_auth=(self.username, self.password),
 150 |                         )
 151 |                         print("Connection successful with username/password")
 152 |                         return True
 153 |                     except Exception as e:
 154 |                         error_msg = f"Failed basic_auth with username/password: {type(e).__name__}: {str(e)}"
 155 |                         print(error_msg)
 156 |                         error_messages.append(error_msg)
 157 | 
 158 |                 if self.username and self.token:
 159 |                     try:
 160 |                         print(f"Trying basic_auth with username and API token")
 161 |                         self.client = JIRA(
 162 |                             server=self.server_url,
 163 |                             basic_auth=(self.username, self.token),
 164 |                         )
 165 |                         print("Connection successful with username/token")
 166 |                         return True
 167 |                     except Exception as e:
 168 |                         error_msg = f"Failed basic_auth with username/token: {type(e).__name__}: {str(e)}"
 169 |                         print(error_msg)
 170 |                         error_messages.append(error_msg)
 171 | 
 172 |                 print("Error: Username and password/token required for basic auth")
 173 |                 error_messages.append(
 174 |                     "Username and password/token required for basic auth"
 175 |                 )
 176 | 
 177 |             elif self.auth_method == "token_auth":
 178 |                 # Token auth - just need the token
 179 |                 if self.token:
 180 |                     try:
 181 |                         print(f"Trying token_auth with token")
 182 |                         self.client = JIRA(
 183 |                             server=self.server_url, token_auth=self.token
 184 |                         )
 185 |                         print("Connection successful with token_auth")
 186 |                         return True
 187 |                     except Exception as e:
 188 |                         error_msg = f"Failed token_auth: {type(e).__name__}: {str(e)}"
 189 |                         print(error_msg)
 190 |                         error_messages.append(error_msg)
 191 |                 else:
 192 |                     print("Error: Token required for token auth")
 193 |                     error_messages.append("Token required for token auth")
 194 | 
 195 |             # If we're here and have a token, try using it with basic_auth for Jira Cloud
 196 |             # (even if auth_method wasn't basic_auth)
 197 |             if self.token and self.username and not self.client:
 198 |                 try:
 199 |                     print(f"Trying fallback to basic_auth with username and token")
 200 |                     self.client = JIRA(
 201 |                         server=self.server_url, basic_auth=(self.username, self.token)
 202 |                     )
 203 |                     print("Connection successful with fallback basic_auth")
 204 |                     return True
 205 |                 except Exception as e:
 206 |                     error_msg = (
 207 |                         f"Failed fallback to basic_auth: {type(e).__name__}: {str(e)}"
 208 |                     )
 209 |                     print(error_msg)
 210 |                     error_messages.append(error_msg)
 211 | 
 212 |             # If we're here and have a token, try using token_auth as a fallback
 213 |             # (even if auth_method wasn't token_auth)
 214 |             if self.token and not self.client:
 215 |                 try:
 216 |                     print(f"Trying fallback to token_auth")
 217 |                     self.client = JIRA(server=self.server_url, token_auth=self.token)
 218 |                     print("Connection successful with fallback token_auth")
 219 |                     return True
 220 |                 except Exception as e:
 221 |                     error_msg = (
 222 |                         f"Failed fallback to token_auth: {type(e).__name__}: {str(e)}"
 223 |                     )
 224 |                     print(error_msg)
 225 |                     error_messages.append(error_msg)
 226 | 
 227 |             # Last resort: try anonymous access
 228 |             try:
 229 |                 print(f"Trying anonymous access as last resort")
 230 |                 self.client = JIRA(server=self.server_url)
 231 |                 print("Connection successful with anonymous access")
 232 |                 return True
 233 |             except Exception as e:
 234 |                 error_msg = f"Failed anonymous access: {type(e).__name__}: {str(e)}"
 235 |                 print(error_msg)
 236 |                 error_messages.append(error_msg)
 237 | 
 238 |             # If we got here, all connection attempts failed
 239 |             print(f"All connection attempts failed: {', '.join(error_messages)}")
 240 |             return False
 241 | 
 242 |         except Exception as e:
 243 |             error_msg = f"Unexpected error in connect(): {type(e).__name__}: {str(e)}"
 244 |             print(error_msg)
 245 |             error_messages.append(error_msg)
 246 |             return False
 247 | 
 248 |     def _get_v3_api_client(self) -> JiraV3APIClient:
 249 |         """Get or create a v3 API client instance"""
 250 |         if not self._v3_api_client:
 251 |             self._v3_api_client = JiraV3APIClient(
 252 |                 server_url=self.server_url,
 253 |                 username=self.username,
 254 |                 password=self.password,
 255 |                 token=self.token,
 256 |             )
 257 |         return self._v3_api_client
 258 | 
 259 |     async def get_jira_projects(self) -> List[JiraProjectResult]:
 260 |         """Get all accessible Jira projects using v3 REST API"""
 261 |         logger.info("Starting get_jira_projects...")
 262 |         all_projects_data = []
 263 |         start_at = 0
 264 |         max_results = 50
 265 |         page_count = 0
 266 | 
 267 |         while True:
 268 |             page_count += 1
 269 |             logger.info(
 270 |                 f"Pagination loop, page {page_count}: startAt={start_at}, maxResults={max_results}"
 271 |             )
 272 | 
 273 |             try:
 274 |                 response = await self._v3_api_client.get_projects(
 275 |                     start_at=start_at, max_results=max_results
 276 |                 )
 277 | 
 278 |                 projects = response.get("values", [])
 279 |                 if not projects:
 280 |                     logger.info("No more projects returned. Breaking pagination loop.")
 281 |                     break
 282 | 
 283 |                 all_projects_data.extend(projects)
 284 | 
 285 |                 if response.get("isLast", False):
 286 |                     logger.info("'isLast' is True. Breaking pagination loop.")
 287 |                     break
 288 | 
 289 |                 start_at += len(projects)
 290 | 
 291 |                 # Yield control to the event loop to prevent deadlocks in the MCP framework.
 292 |                 await asyncio.sleep(0)
 293 | 
 294 |             except Exception as e:
 295 |                 logger.error(
 296 |                     "Error inside get_jira_projects pagination loop", exc_info=True
 297 |                 )
 298 |                 raise
 299 | 
 300 |         logger.info(
 301 |             f"Finished get_jira_projects. Total projects found: {len(all_projects_data)}"
 302 |         )
 303 | 
 304 |         results = []
 305 |         for p in all_projects_data:
 306 |             results.append(
 307 |                 JiraProjectResult(
 308 |                     key=p.get("key"),
 309 |                     name=p.get("name"),
 310 |                     id=str(p.get("id")),
 311 |                     lead=(p.get("lead") or {}).get("displayName"),
 312 |                 )
 313 |             )
 314 |             logger.info(f"Added project {p.get('key')} to results")
 315 |         logger.info(f"Returning {len(results)} projects")
 316 |         sys.stdout.flush()  # Flush stdout to ensure it's sent to MCP, otherwise hang occurs
 317 |         return results
 318 | 
 319 |     def get_jira_issue(self, issue_key: str) -> JiraIssueResult:
 320 |         """Get details for a specific issue by key"""
 321 |         if not self.client:
 322 |             if not self.connect():
 323 |                 # Connection failed - provide clear error message
 324 |                 raise ValueError(
 325 |                     f"Failed to connect to Jira server at {self.server_url}. Check your authentication credentials."
 326 |                 )
 327 | 
 328 |         try:
 329 |             issue = self.client.issue(issue_key)
 330 | 
 331 |             # Extract comments if available
 332 |             comments = []
 333 |             if hasattr(issue.fields, "comment") and hasattr(
 334 |                 issue.fields.comment, "comments"
 335 |             ):
 336 |                 for comment in issue.fields.comment.comments:
 337 |                     comments.append(
 338 |                         {
 339 |                             "author": (
 340 |                                 getattr(
 341 |                                     comment.author, "displayName", str(comment.author)
 342 |                                 )
 343 |                                 if hasattr(comment, "author")
 344 |                                 else "Unknown"
 345 |                             ),
 346 |                             "body": comment.body,
 347 |                             "created": comment.created,
 348 |                         }
 349 |                     )
 350 | 
 351 |             # Create a fields dictionary with custom fields
 352 |             fields = {}
 353 |             for field_name in dir(issue.fields):
 354 |                 if not field_name.startswith("_") and field_name not in [
 355 |                     "comment",
 356 |                     "attachment",
 357 |                     "summary",
 358 |                     "description",
 359 |                     "status",
 360 |                     "assignee",
 361 |                     "reporter",
 362 |                     "created",
 363 |                     "updated",
 364 |                 ]:
 365 |                     value = getattr(issue.fields, field_name)
 366 |                     if value is not None:
 367 |                         # Handle special field types
 368 |                         if hasattr(value, "name"):
 369 |                             fields[field_name] = value.name
 370 |                         elif hasattr(value, "value"):
 371 |                             fields[field_name] = value.value
 372 |                         elif isinstance(value, list):
 373 |                             if len(value) > 0:
 374 |                                 if hasattr(value[0], "name"):
 375 |                                     fields[field_name] = [item.name for item in value]
 376 |                                 else:
 377 |                                     fields[field_name] = value
 378 |                         else:
 379 |                             fields[field_name] = str(value)
 380 | 
 381 |             return JiraIssueResult(
 382 |                 key=issue.key,
 383 |                 summary=issue.fields.summary,
 384 |                 description=issue.fields.description,
 385 |                 status=(
 386 |                     issue.fields.status.name
 387 |                     if hasattr(issue.fields, "status")
 388 |                     else None
 389 |                 ),
 390 |                 assignee=(
 391 |                     issue.fields.assignee.displayName
 392 |                     if hasattr(issue.fields, "assignee") and issue.fields.assignee
 393 |                     else None
 394 |                 ),
 395 |                 reporter=(
 396 |                     issue.fields.reporter.displayName
 397 |                     if hasattr(issue.fields, "reporter") and issue.fields.reporter
 398 |                     else None
 399 |                 ),
 400 |                 created=(
 401 |                     issue.fields.created if hasattr(issue.fields, "created") else None
 402 |                 ),
 403 |                 updated=(
 404 |                     issue.fields.updated if hasattr(issue.fields, "updated") else None
 405 |                 ),
 406 |                 fields=fields,
 407 |                 comments=comments,
 408 |             )
 409 |         except Exception as e:
 410 |             print(f"Failed to get issue {issue_key}: {type(e).__name__}: {str(e)}")
 411 |             raise ValueError(
 412 |                 f"Failed to get issue {issue_key}: {type(e).__name__}: {str(e)}"
 413 |             )
 414 | 
 415 |     async def search_jira_issues(
 416 |         self, jql: str, max_results: int = 10
 417 |     ) -> List[JiraIssueResult]:
 418 |         """Search for issues using JQL via v3 REST API with pagination support"""
 419 |         logger.info("Starting search_jira_issues...")
 420 | 
 421 |         try:
 422 |             # Use v3 API client
 423 |             v3_client = self._get_v3_api_client()
 424 |             
 425 |             # Collect all issues from all pages
 426 |             all_issues = []
 427 |             start_at = 0
 428 |             page_size = min(max_results, 100)  # Jira typically limits to 100 per page
 429 |             
 430 |             while True:
 431 |                 logger.debug(f"Fetching page starting at {start_at} with page size {page_size}")
 432 |                 response_data = await v3_client.search_issues(
 433 |                     jql=jql, 
 434 |                     start_at=start_at,
 435 |                     max_results=page_size
 436 |                 )
 437 | 
 438 |                 # Extract issues from current page
 439 |                 page_issues = response_data.get("issues", [])
 440 |                 all_issues.extend(page_issues)
 441 |                 
 442 |                 logger.debug(f"Retrieved {len(page_issues)} issues from current page. Total so far: {len(all_issues)}")
 443 | 
 444 |                 # Check if we've reached the user's max_results limit
 445 |                 if len(all_issues) >= max_results:
 446 |                     # Trim to exact max_results if we exceeded it
 447 |                     all_issues = all_issues[:max_results]
 448 |                     logger.debug(f"Reached max_results limit of {max_results}, stopping pagination")
 449 |                     break
 450 | 
 451 |                 # Check if this is the last page according to API
 452 |                 is_last = response_data.get("isLast", True)
 453 |                 if is_last:
 454 |                     logger.debug("API indicates this is the last page, stopping pagination")
 455 |                     break
 456 | 
 457 |                 # If we have more pages, prepare for next iteration
 458 |                 start_at = len(all_issues)  # Use actual number of issues retrieved so far
 459 |                 
 460 |                 # Adjust page size for next request to not exceed max_results
 461 |                 remaining_needed = max_results - len(all_issues)
 462 |                 page_size = min(remaining_needed, 100)
 463 | 
 464 |             # Return raw issues list for full JSON data
 465 |             logger.info(f"Returning raw issues ({len(all_issues)}) for JQL: {jql}")
 466 |             return all_issues
 467 | 
 468 | 
 469 |         except Exception as e:
 470 |             error_msg = f"Failed to search issues: {type(e).__name__}: {str(e)}"
 471 |             logger.error(error_msg, exc_info=True)
 472 |             print(error_msg)
 473 |             raise ValueError(error_msg)
 474 | 
 475 |     async def create_jira_issue(
 476 |         self,
 477 |         project: str,
 478 |         summary: str,
 479 |         description: str,
 480 |         issue_type: str,
 481 |         fields: Optional[Dict[str, Any]] = None,
 482 |     ) -> JiraIssueResult:
 483 |         """Create a new Jira issue using v3 REST API
 484 | 
 485 |         Args:
 486 |             project: Project key (e.g., 'PROJ')
 487 |             summary: Issue summary/title
 488 |             description: Issue description
 489 |             issue_type: Issue type - common values include 'Bug', 'Task', 'Story', 'Epic', 'New Feature', 'Improvement'
 490 |                        Note: Available issue types vary by Jira instance and project
 491 |             fields: Optional additional fields dictionary
 492 | 
 493 |         Returns:
 494 |             JiraIssueResult object with the created issue details
 495 | 
 496 |         Example:
 497 |             # Create a bug
 498 |             await create_jira_issue(
 499 |                 project='PROJ',
 500 |                 summary='Login button not working',
 501 |                 description='The login button on the homepage is not responding to clicks',
 502 |                 issue_type='Bug'
 503 |             )
 504 | 
 505 |             # Create a task with custom fields
 506 |             await create_jira_issue(
 507 |                 project='PROJ',
 508 |                 summary='Update documentation',
 509 |                 description='Update API documentation with new endpoints',
 510 |                 issue_type='Task',
 511 |                 fields={
 512 |                     'assignee': 'jsmith',
 513 |                     'labels': ['documentation', 'api'],
 514 |                     'priority': {'name': 'High'}
 515 |                 }
 516 |             )
 517 |         """
 518 |         logger.info("Starting create_jira_issue...")
 519 | 
 520 |         try:
 521 |             # Create a properly formatted issue dictionary
 522 |             issue_dict = {}
 523 | 
 524 |             # Process required fields first
 525 |             # Project field - required
 526 |             if isinstance(project, str):
 527 |                 issue_dict["project"] = {"key": project}
 528 |             else:
 529 |                 issue_dict["project"] = project
 530 | 
 531 |             # Summary - required
 532 |             issue_dict["summary"] = summary
 533 | 
 534 |             # Description
 535 |             if description:
 536 |                 issue_dict["description"] = description
 537 | 
 538 |             # Issue type - required, with validation for common issue types
 539 |             logger.info(
 540 |                 f"Processing issue_type: '{issue_type}' (type: {type(issue_type)})"
 541 |             )
 542 |             common_types = [
 543 |                 "bug",
 544 |                 "task",
 545 |                 "story",
 546 |                 "epic",
 547 |                 "improvement",
 548 |                 "newfeature",
 549 |                 "new feature",
 550 |             ]
 551 | 
 552 |             if isinstance(issue_type, str):
 553 |                 # Check for common issue type variants and fix case-sensitivity issues
 554 |                 issue_type_lower = issue_type.lower()
 555 | 
 556 |                 if issue_type_lower in common_types:
 557 |                     # Convert first letter to uppercase for standard Jira types
 558 |                     issue_type_proper = issue_type_lower.capitalize()
 559 |                     if (
 560 |                         issue_type_lower == "new feature"
 561 |                         or issue_type_lower == "newfeature"
 562 |                     ):
 563 |                         issue_type_proper = "New Feature"
 564 | 
 565 |                     logger.info(
 566 |                         f"Note: Converting issue type from '{issue_type}' to '{issue_type_proper}'"
 567 |                     )
 568 |                     issue_dict["issuetype"] = {"name": issue_type_proper}
 569 |                 else:
 570 |                     # Use the type as provided - some Jira instances have custom types
 571 |                     issue_dict["issuetype"] = {"name": issue_type}
 572 |             else:
 573 |                 issue_dict["issuetype"] = issue_type
 574 | 
 575 |             # Add any additional fields with proper type handling
 576 |             if fields:
 577 |                 for key, value in fields.items():
 578 |                     # Skip fields we've already processed
 579 |                     if key in [
 580 |                         "project",
 581 |                         "summary",
 582 |                         "description",
 583 |                         "issuetype",
 584 |                         "issue_type",
 585 |                     ]:
 586 |                         continue
 587 | 
 588 |                     # Handle special fields that require specific formats
 589 |                     if key == "assignees" or key == "assignee":
 590 |                         # Convert string to array for assignees or proper format for assignee
 591 |                         if isinstance(value, str):
 592 |                             if key == "assignees":
 593 |                                 issue_dict[key] = [value] if value else []
 594 |                             else:  # assignee
 595 |                                 issue_dict[key] = {"name": value} if value else None
 596 |                         elif isinstance(value, list) and key == "assignee" and value:
 597 |                             # If assignee is a list but should be a dict with name
 598 |                             issue_dict[key] = {"name": value[0]}
 599 |                         else:
 600 |                             issue_dict[key] = value
 601 |                     elif key == "labels":
 602 |                         # Convert string to array for labels
 603 |                         if isinstance(value, str):
 604 |                             issue_dict[key] = [value] if value else []
 605 |                         else:
 606 |                             issue_dict[key] = value
 607 |                     elif key == "milestone":
 608 |                         # Convert string to number for milestone
 609 |                         if isinstance(value, str) and value.isdigit():
 610 |                             issue_dict[key] = int(value)
 611 |                         else:
 612 |                             issue_dict[key] = value
 613 |                     else:
 614 |                         issue_dict[key] = value
 615 | 
 616 |             # Use v3 API client
 617 |             v3_client = self._get_v3_api_client()
 618 |             response_data = await v3_client.create_issue(fields=issue_dict)
 619 | 
 620 |             # Extract issue details from v3 API response
 621 |             issue_key = response_data.get("key")
 622 |             issue_id = response_data.get("id")
 623 | 
 624 |             logger.info(f"Successfully created issue {issue_key} (ID: {issue_id})")
 625 | 
 626 |             # Return JiraIssueResult with the created issue details
 627 |             # For v3 API, we return what we have from the create response
 628 |             return JiraIssueResult(
 629 |                 key=issue_key,
 630 |                 summary=summary,  # Use the summary we provided
 631 |                 description=description,  # Use the description we provided
 632 |                 status="Open",  # Default status for new issues
 633 |             )
 634 | 
 635 |         except Exception as e:
 636 |             error_msg = f"Failed to create issue: {type(e).__name__}: {str(e)}"
 637 |             logger.error(error_msg, exc_info=True)
 638 | 
 639 |             # Enhanced error handling for issue type errors
 640 |             if "issuetype" in str(e).lower() or "issue type" in str(e).lower():
 641 |                 logger.info(
 642 |                     "Issue type error detected, trying to provide helpful suggestions..."
 643 |                 )
 644 |                 try:
 645 |                     project_key = (
 646 |                         project if isinstance(project, str) else project.get("key")
 647 |                     )
 648 |                     if project_key:
 649 |                         issue_types = await self.get_jira_project_issue_types(
 650 |                             project_key
 651 |                         )
 652 |                         type_names = [t.get("name") for t in issue_types]
 653 |                         logger.info(
 654 |                             f"Available issue types for project {project_key}: {', '.join(type_names)}"
 655 |                         )
 656 | 
 657 |                         # Try to find the closest match
 658 |                         attempted_type = issue_type
 659 |                         closest = None
 660 |                         attempted_lower = attempted_type.lower()
 661 |                         for t in type_names:
 662 |                             if (
 663 |                                 attempted_lower in t.lower()
 664 |                                 or t.lower() in attempted_lower
 665 |                             ):
 666 |                                 closest = t
 667 |                                 break
 668 | 
 669 |                         if closest:
 670 |                             logger.info(
 671 |                                 f"The closest match to '{attempted_type}' is '{closest}'"
 672 |                             )
 673 |                             error_msg += f" Available types: {', '.join(type_names)}. Closest match: '{closest}'"
 674 |                         else:
 675 |                             error_msg += f" Available types: {', '.join(type_names)}"
 676 |                 except Exception as fetch_error:
 677 |                     logger.error(f"Could not fetch issue types: {str(fetch_error)}")
 678 | 
 679 |             raise ValueError(error_msg)
 680 | 
 681 | 
 682 | 
 683 |             # Re-raise the exception with more details
 684 |             if "issuetype" in error_message.lower():
 685 |                 raise ValueError(
 686 |                     f"Invalid issue type '{issue_dict.get('issuetype', {}).get('name', 'Unknown')}'. "
 687 |                     + "Use get_jira_project_issue_types(project_key) to get valid types."
 688 |                 )
 689 |             raise
 690 | 
 691 |             return JiraIssueResult(
 692 |                 key=new_issue.key,
 693 |                 summary=new_issue.fields.summary,
 694 |                 description=new_issue.fields.description,
 695 |                 status=(
 696 |                     new_issue.fields.status.name
 697 |                     if hasattr(new_issue.fields, "status")
 698 |                     else None
 699 |                 ),
 700 |             )
 701 |         except Exception as e:
 702 |             print(f"Failed to create issue: {type(e).__name__}: {str(e)}")
 703 |             raise ValueError(f"Failed to create issue: {type(e).__name__}: {str(e)}")
 704 | 
 705 |     async def create_jira_issues(
 706 |         self, field_list: List[Dict[str, Any]], prefetch: bool = True
 707 |     ) -> List[Dict[str, Any]]:
 708 |         """Bulk create new Jira issues using v3 REST API.
 709 | 
 710 |         Parameters:
 711 |             field_list (List[Dict[str, Any]]): a list of dicts each containing field names and the values to use.
 712 |                                              Each dict is an individual issue to create.
 713 |             prefetch (bool): True reloads the created issue Resource so all of its data is present in the value returned (Default: True)
 714 | 
 715 |         Returns:
 716 |             List[Dict[str, Any]]: List of created issues with their details
 717 | 
 718 |         Issue Types:
 719 |             Common issue types include: 'Bug', 'Task', 'Story', 'Epic', 'New Feature', 'Improvement'
 720 |             Note: Available issue types vary by Jira instance and project
 721 | 
 722 |         Example:
 723 |             # Create multiple issues in bulk
 724 |             await create_jira_issues([
 725 |                 {
 726 |                     'project': 'PROJ',
 727 |                     'summary': 'Implement user authentication',
 728 |                     'description': 'Add login and registration functionality',
 729 |                     'issue_type': 'Story'  # Note: case-sensitive, match to your Jira instance types
 730 |                 },
 731 |                 {
 732 |                     'project': 'PROJ',
 733 |                     'summary': 'Fix navigation bar display on mobile',
 734 |                     'description': 'Navigation bar is not displaying correctly on mobile devices',
 735 |                     'issue_type': 'Bug',
 736 |                     'priority': {'name': 'High'},
 737 |                     'labels': ['mobile', 'ui']
 738 |                 }
 739 |             ])
 740 |         """
 741 |         logger.info("Starting create_jira_issues...")
 742 | 
 743 |         try:
 744 |             # Process each field dict to ensure proper formatting for v3 API
 745 |             processed_field_list = []
 746 |             for fields in field_list:
 747 |                 # Create a properly formatted issue dictionary
 748 |                 issue_dict = {}
 749 | 
 750 |                 # Process required fields first to ensure they exist
 751 |                 # Project field - required
 752 |                 if "project" not in fields:
 753 |                     raise ValueError("Each issue must have a 'project' field")
 754 |                 project_value = fields["project"]
 755 |                 if isinstance(project_value, str):
 756 |                     issue_dict["project"] = {"key": project_value}
 757 |                 else:
 758 |                     issue_dict["project"] = project_value
 759 | 
 760 |                 # Summary field - required
 761 |                 if "summary" not in fields:
 762 |                     raise ValueError("Each issue must have a 'summary' field")
 763 |                 issue_dict["summary"] = fields["summary"]
 764 | 
 765 |                 # Description field - convert to ADF format for v3 API if it's a simple string
 766 |                 if "description" in fields:
 767 |                     description = fields["description"]
 768 |                     if isinstance(description, str):
 769 |                         # Convert simple string to Atlassian Document Format
 770 |                         issue_dict["description"] = {
 771 |                             "type": "doc",
 772 |                             "version": 1,
 773 |                             "content": [
 774 |                                 {
 775 |                                     "type": "paragraph",
 776 |                                     "content": [
 777 |                                         {
 778 |                                             "type": "text",
 779 |                                             "text": description
 780 |                                         }
 781 |                                     ]
 782 |                                 }
 783 |                             ]
 784 |                         }
 785 |                     else:
 786 |                         # Assume it's already in ADF format
 787 |                         issue_dict["description"] = description
 788 | 
 789 |                 # Issue type field - required, handle both 'issuetype' and 'issue_type'
 790 |                 issue_type = None
 791 |                 if "issuetype" in fields:
 792 |                     issue_type = fields["issuetype"]
 793 |                 elif "issue_type" in fields:
 794 |                     issue_type = fields["issue_type"]
 795 |                 else:
 796 |                     raise ValueError(
 797 |                         "Each issue must have an 'issuetype' or 'issue_type' field"
 798 |                     )
 799 | 
 800 |                 # Check for common issue type variants and fix case-sensitivity issues
 801 |                 logger.debug(
 802 |                     f"Processing bulk issue_type: '{issue_type}' (type: {type(issue_type)})"
 803 |                 )
 804 |                 common_types = [
 805 |                     "bug",
 806 |                     "task",
 807 |                     "story",
 808 |                     "epic",
 809 |                     "improvement",
 810 |                     "newfeature",
 811 |                     "new feature",
 812 |                 ]
 813 | 
 814 |                 if isinstance(issue_type, str):
 815 |                     issue_type_lower = issue_type.lower()
 816 | 
 817 |                     if issue_type_lower in common_types:
 818 |                         # Convert first letter to uppercase for standard Jira types
 819 |                         issue_type_proper = issue_type_lower.capitalize()
 820 |                         if (
 821 |                             issue_type_lower == "new feature"
 822 |                             or issue_type_lower == "newfeature"
 823 |                         ):
 824 |                             issue_type_proper = "New Feature"
 825 | 
 826 |                         logger.debug(
 827 |                             f"Converting issue type from '{issue_type}' to '{issue_type_proper}'"
 828 |                         )
 829 |                         issue_dict["issuetype"] = {"name": issue_type_proper}
 830 |                     else:
 831 |                         # Use the type as provided - some Jira instances have custom types
 832 |                         issue_dict["issuetype"] = {"name": issue_type}
 833 |                 else:
 834 |                     issue_dict["issuetype"] = issue_type
 835 | 
 836 |                 # Process other fields
 837 |                 for key, value in fields.items():
 838 |                     if key in [
 839 |                         "project",
 840 |                         "summary",
 841 |                         "description",
 842 |                         "issuetype",
 843 |                         "issue_type",
 844 |                     ]:
 845 |                         # Skip fields we've already processed
 846 |                         continue
 847 | 
 848 |                     # Handle special fields that require specific formats
 849 |                     if key == "assignees" or key == "assignee":
 850 |                         # Convert string to array for assignees or proper format for assignee
 851 |                         if isinstance(value, str):
 852 |                             if key == "assignees":
 853 |                                 issue_dict[key] = [value] if value else []
 854 |                             else:  # assignee
 855 |                                 issue_dict[key] = {"name": value} if value else None
 856 |                         elif isinstance(value, list) and key == "assignee" and value:
 857 |                             # If assignee is a list but should be a dict with name
 858 |                             issue_dict[key] = {"name": value[0]}
 859 |                         else:
 860 |                             issue_dict[key] = value
 861 |                     elif key == "labels":
 862 |                         # Convert string to array for labels
 863 |                         if isinstance(value, str):
 864 |                             issue_dict[key] = [value] if value else []
 865 |                         else:
 866 |                             issue_dict[key] = value
 867 |                     elif key == "milestone":
 868 |                         # Convert string to number for milestone
 869 |                         if isinstance(value, str) and value.isdigit():
 870 |                             issue_dict[key] = int(value)
 871 |                         else:
 872 |                             issue_dict[key] = value
 873 |                     else:
 874 |                         issue_dict[key] = value
 875 | 
 876 |                 # Add to the field list in v3 API format
 877 |                 processed_field_list.append({"fields": issue_dict})
 878 | 
 879 |             logger.debug(f"Processed field list: {json.dumps(processed_field_list, indent=2)}")
 880 | 
 881 |             # Use v3 API client
 882 |             v3_client = self._get_v3_api_client()
 883 |             
 884 |             # Call the bulk create API
 885 |             response_data = await v3_client.bulk_create_issues(processed_field_list)
 886 |             
 887 |             # Process the results to maintain compatibility with existing interface
 888 |             processed_results = []
 889 |             
 890 |             # Handle successful issues
 891 |             if "issues" in response_data:
 892 |                 for issue in response_data["issues"]:
 893 |                     processed_results.append({
 894 |                         "key": issue.get("key"),
 895 |                         "id": issue.get("id"),
 896 |                         "self": issue.get("self"),
 897 |                         "success": True,
 898 |                     })
 899 |             
 900 |             # Handle errors
 901 |             if "errors" in response_data:
 902 |                 for error in response_data["errors"]:
 903 |                     processed_results.append({
 904 |                         "error": error,
 905 |                         "success": False,
 906 |                     })
 907 | 
 908 |             logger.info(f"Successfully processed {len(processed_results)} issue creations")
 909 |             return processed_results
 910 | 
 911 |         except Exception as e:
 912 |             error_msg = f"Failed to create issues in bulk: {type(e).__name__}: {str(e)}"
 913 |             logger.error(error_msg, exc_info=True)
 914 |             print(error_msg)
 915 |             raise ValueError(error_msg)
 916 | 
 917 |     async def add_jira_comment(self, issue_key: str, comment: str) -> Dict[str, Any]:
 918 |         """Add a comment to an issue using v3 REST API"""
 919 |         logger.info("Starting add_jira_comment...")
 920 | 
 921 |         try:
 922 |             # Use v3 API client
 923 |             v3_client = self._get_v3_api_client()
 924 |             comment_result = await v3_client.add_comment(
 925 |                 issue_id_or_key=issue_key,
 926 |                 comment=comment,
 927 |             )
 928 | 
 929 |             # Extract useful information from the v3 API response
 930 |             response_data = {
 931 |                 "id": comment_result.get("id"),
 932 |                 "body": comment_result.get("body", {}),
 933 |                 "created": comment_result.get("created"),
 934 |                 "updated": comment_result.get("updated"),
 935 |             }
 936 | 
 937 |             # Extract author information if available
 938 |             if "author" in comment_result:
 939 |                 author = comment_result["author"]
 940 |                 response_data["author"] = author.get("displayName", "Unknown")
 941 |             else:
 942 |                 response_data["author"] = "Unknown"
 943 | 
 944 |             logger.info(f"Successfully added comment to issue {issue_key}")
 945 |             return response_data
 946 | 
 947 |         except Exception as e:
 948 |             error_msg = (
 949 |                 f"Failed to add comment to {issue_key}: {type(e).__name__}: {str(e)}"
 950 |             )
 951 |             logger.error(error_msg, exc_info=True)
 952 |             print(error_msg)
 953 |             raise ValueError(error_msg)
 954 | 
 955 |     async def get_jira_transitions(self, issue_key: str) -> List[JiraTransitionResult]:
 956 |         """Get available transitions for an issue using v3 REST API"""
 957 |         logger.info("Starting get_jira_transitions...")
 958 | 
 959 |         try:
 960 |             # Use v3 API client
 961 |             v3_client = self._get_v3_api_client()
 962 |             response_data = await v3_client.get_transitions(issue_id_or_key=issue_key)
 963 | 
 964 |             # Extract transitions from response
 965 |             transitions = response_data.get("transitions", [])
 966 | 
 967 |             # Convert to JiraTransitionResult objects maintaining compatibility
 968 |             results = [
 969 |                 JiraTransitionResult(id=transition["id"], name=transition["name"])
 970 |                 for transition in transitions
 971 |             ]
 972 | 
 973 |             logger.info(f"Found {len(results)} transitions for issue {issue_key}")
 974 |             return results
 975 | 
 976 |         except Exception as e:
 977 |             error_msg = f"Failed to get transitions for {issue_key}: {type(e).__name__}: {str(e)}"
 978 |             logger.error(error_msg, exc_info=True)
 979 |             print(error_msg)
 980 |             raise ValueError(error_msg)
 981 | 
 982 |     async def transition_jira_issue(
 983 |         self,
 984 |         issue_key: str,
 985 |         transition_id: str,
 986 |         comment: Optional[str] = None,
 987 |         fields: Optional[Dict[str, Any]] = None,
 988 |     ) -> bool:
 989 |         """Transition an issue to a new state using v3 REST API"""
 990 |         logger.info("Starting transition_jira_issue...")
 991 | 
 992 |         try:
 993 |             # Use v3 API client
 994 |             v3_client = self._get_v3_api_client()
 995 |             await v3_client.transition_issue(
 996 |                 issue_id_or_key=issue_key,
 997 |                 transition_id=transition_id,
 998 |                 fields=fields,
 999 |                 comment=comment,
1000 |             )
1001 | 
1002 |             logger.info(
1003 |                 f"Successfully transitioned issue {issue_key} to transition {transition_id}"
1004 |             )
1005 |             return True
1006 | 
1007 |         except Exception as e:
1008 |             error_msg = (
1009 |                 f"Failed to transition {issue_key}: {type(e).__name__}: {str(e)}"
1010 |             )
1011 |             logger.error(error_msg, exc_info=True)
1012 |             print(error_msg)
1013 |             raise ValueError(error_msg)
1014 | 
1015 |     async def get_jira_project_issue_types(
1016 |         self, project_key: str
1017 |     ) -> List[Dict[str, Any]]:
1018 |         """Get all available issue types for a specific project using v3 REST API
1019 | 
1020 |         Args:
1021 |             project_key: The project key (e.g., 'PROJ') - kept for backward compatibility,
1022 |                         but the new API returns all issue types for the user
1023 | 
1024 |         Returns:
1025 |             List of issue type dictionaries with name, id, and description
1026 | 
1027 |         Example:
1028 |             get_jira_project_issue_types('PROJ')  # Returns all issue types accessible to user
1029 |         """
1030 |         logger.info("Starting get_jira_project_issue_types...")
1031 | 
1032 |         try:
1033 |             # Use v3 API client to get all issue types
1034 |             v3_client = self._get_v3_api_client()
1035 |             response_data = await v3_client.get_issue_types()
1036 | 
1037 |             # The new API returns the issue types directly as a list, not wrapped in an object
1038 |             issue_types_data = (
1039 |                 response_data
1040 |                 if isinstance(response_data, list)
1041 |                 else response_data.get("issueTypes", [])
1042 |             )
1043 | 
1044 |             # Convert to the expected format maintaining compatibility
1045 |             issue_types = []
1046 |             for issuetype in issue_types_data:
1047 |                 issue_types.append(
1048 |                     {
1049 |                         "id": issuetype.get("id"),
1050 |                         "name": issuetype.get("name"),
1051 |                         "description": issuetype.get("description"),
1052 |                     }
1053 |                 )
1054 | 
1055 |             logger.info(
1056 |                 f"Found {len(issue_types)} issue types (project_key: {project_key})"
1057 |             )
1058 |             return issue_types
1059 | 
1060 |         except Exception as e:
1061 |             error_msg = f"Failed to get issue types: {type(e).__name__}: {str(e)}"
1062 |             logger.error(error_msg, exc_info=True)
1063 |             print(error_msg)
1064 |             raise ValueError(error_msg)
1065 | 
1066 |     async def create_jira_project(
1067 |         self,
1068 |         key: str,
1069 |         name: Optional[str] = None,
1070 |         assignee: Optional[str] = None,
1071 |         ptype: str = "software",
1072 |         template_name: Optional[str] = None,
1073 |         avatarId: Optional[int] = None,
1074 |         issueSecurityScheme: Optional[int] = None,
1075 |         permissionScheme: Optional[int] = None,
1076 |         projectCategory: Optional[int] = None,
1077 |         notificationScheme: Optional[int] = None,
1078 |         categoryId: Optional[int] = None,
1079 |         url: str = "",
1080 |     ) -> JiraProjectResult:
1081 |         """Create a project using Jira's v3 REST API
1082 | 
1083 |         Args:
1084 |             key: Project key (required) - must match Jira project key requirements
1085 |             name: Project name (defaults to key if not provided)
1086 |             assignee: Lead account ID or username
1087 |             ptype: Project type key ('software', 'business', 'service_desk')
1088 |             template_name: Project template key for creating from templates
1089 |             avatarId: ID of the avatar to use for the project
1090 |             issueSecurityScheme: ID of the issue security scheme
1091 |             permissionScheme: ID of the permission scheme
1092 |             projectCategory: ID of the project category
1093 |             notificationScheme: ID of the notification scheme
1094 |             categoryId: Same as projectCategory (alternative parameter)
1095 |             url: URL for project information/documentation
1096 | 
1097 |         Returns:
1098 |             JiraProjectResult with the created project details
1099 | 
1100 |         Note:
1101 |             This method uses Jira's v3 REST API endpoint: POST /rest/api/3/project
1102 | 
1103 |         Example:
1104 |             # Create a basic software project
1105 |             create_jira_project(
1106 |                 key='PROJ',
1107 |                 name='My Project',
1108 |                 ptype='software'
1109 |             )
1110 | 
1111 |             # Create with template
1112 |             create_jira_project(
1113 |                 key='BUSI',
1114 |                 name='Business Project',
1115 |                 ptype='business',
1116 |                 template_name='com.atlassian.jira-core-project-templates:jira-core-simplified-task-tracking'
1117 |             )
1118 |         """
1119 |         if not key:
1120 |             raise ValueError("Project key is required")
1121 | 
1122 |         try:
1123 |             # Get the v3 API client
1124 |             v3_client = self._get_v3_api_client()
1125 | 
1126 |             # Create project using v3 API
1127 |             response_data = await v3_client.create_project(
1128 |                 key=key,
1129 |                 name=name,
1130 |                 assignee=assignee,
1131 |                 ptype=ptype,
1132 |                 template_name=template_name,
1133 |                 avatarId=avatarId,
1134 |                 issueSecurityScheme=issueSecurityScheme,
1135 |                 permissionScheme=permissionScheme,
1136 |                 projectCategory=projectCategory,
1137 |                 notificationScheme=notificationScheme,
1138 |                 categoryId=categoryId,
1139 |                 url=url,
1140 |             )
1141 | 
1142 |             # Extract project details from response
1143 |             project_id = response_data.get("id", "0")
1144 |             project_key = response_data.get("key", key)
1145 | 
1146 |             # For lead information, we would need to make another API call
1147 |             # For now, return None for lead as it's optional in our result model
1148 |             lead = None
1149 | 
1150 |             return JiraProjectResult(
1151 |                 key=project_key, name=name or key, id=str(project_id), lead=lead
1152 |             )
1153 | 
1154 |         except Exception as e:
1155 |             error_msg = str(e)
1156 |             print(f"Error creating project with v3 API: {error_msg}")
1157 |             raise ValueError(f"Error creating project: {error_msg}")
1158 | 
1159 | 
1160 | async def serve(
1161 |     server_url: Optional[str] = None,
1162 |     auth_method: Optional[str] = None,
1163 |     username: Optional[str] = None,
1164 |     password: Optional[str] = None,
1165 |     token: Optional[str] = None,
1166 | ) -> None:
1167 |     server = Server("mcp-jira")
1168 |     jira_server = JiraServer(
1169 |         server_url=server_url,
1170 |         auth_method=auth_method,
1171 |         username=username,
1172 |         password=password,
1173 |         token=token,
1174 |     )
1175 | 
1176 |     @server.list_tools()
1177 |     async def list_tools() -> list[Tool]:
1178 |         """List available Jira tools."""
1179 |         return [
1180 |             Tool(
1181 |                 name=JiraTools.GET_PROJECTS.value,
1182 |                 description="Get all accessible Jira projects",
1183 |                 inputSchema={"type": "object", "properties": {}, "required": []},
1184 |             ),
1185 |             Tool(
1186 |                 name=JiraTools.GET_ISSUE.value,
1187 |                 description="Get details for a specific Jira issue by key",
1188 |                 inputSchema={
1189 |                     "type": "object",
1190 |                     "properties": {
1191 |                         "issue_key": {
1192 |                             "type": "string",
1193 |                             "description": "The issue key (e.g., PROJECT-123)",
1194 |                         }
1195 |                     },
1196 |                     "required": ["issue_key"],
1197 |                 },
1198 |             ),
1199 |             Tool(
1200 |                 name=JiraTools.SEARCH_ISSUES.value,
1201 |                 description="Search for Jira issues using JQL (Jira Query Language)",
1202 |                 inputSchema={
1203 |                     "type": "object",
1204 |                     "properties": {
1205 |                         "jql": {
1206 |                             "type": "string",
1207 |                             "description": "JQL query string (e.g., 'project = MYPROJ AND status = \"In Progress\"')",
1208 |                         },
1209 |                         "max_results": {
1210 |                             "type": "integer",
1211 |                             "description": "Maximum number of results to return (default: 10)",
1212 |                         },
1213 |                     },
1214 |                     "required": ["jql"],
1215 |                 },
1216 |             ),
1217 |             Tool(
1218 |                 name=JiraTools.CREATE_ISSUE.value,
1219 |                 description="Create a new Jira issue. Common issue types include 'Bug', 'Task', 'Story', 'Epic' (capitalization handled automatically)",
1220 |                 inputSchema={
1221 |                     "type": "object",
1222 |                     "properties": {
1223 |                         "project": {
1224 |                             "type": "string",
1225 |                             "description": "Project key (e.g., 'MYPROJ')",
1226 |                         },
1227 |                         "summary": {
1228 |                             "type": "string",
1229 |                             "description": "Issue summary/title",
1230 |                         },
1231 |                         "description": {
1232 |                             "type": "string",
1233 |                             "description": "Issue description",
1234 |                         },
1235 |                         "issue_type": {
1236 |                             "type": "string",
1237 |                             "description": "Issue type (e.g., 'Bug', 'Task', 'Story', 'Epic', 'New Feature', 'Improvement'). IMPORTANT: Types are case-sensitive and vary by Jira instance.",
1238 |                         },
1239 |                         "fields": {
1240 |                             "type": "object",
1241 |                             "description": "Additional fields for the issue (optional)",
1242 |                         },
1243 |                     },
1244 |                     "required": ["project", "summary", "description", "issue_type"],
1245 |                 },
1246 |             ),
1247 |             Tool(
1248 |                 name=JiraTools.CREATE_ISSUES.value,
1249 |                 description="Bulk create new Jira issues. IMPORTANT: For 'issue_type', use the exact case-sensitive types in your Jira instance (common: 'Bug', 'Task', 'Story', 'Epic')",
1250 |                 inputSchema={
1251 |                     "type": "object",
1252 |                     "properties": {
1253 |                         "field_list": {
1254 |                             "type": "array",
1255 |                             "description": "A list of field dictionaries, each representing an issue to create",
1256 |                             "items": {
1257 |                                 "type": "object",
1258 |                                 "description": "Field dictionary for a single issue",
1259 |                             },
1260 |                         },
1261 |                         "prefetch": {
1262 |                             "type": "boolean",
1263 |                             "description": "Whether to reload created issues (default: true)",
1264 |                         },
1265 |                     },
1266 |                     "required": ["field_list"],
1267 |                 },
1268 |             ),
1269 |             Tool(
1270 |                 name=JiraTools.ADD_COMMENT.value,
1271 |                 description="Add a comment to a Jira issue",
1272 |                 inputSchema={
1273 |                     "type": "object",
1274 |                     "properties": {
1275 |                         "issue_key": {
1276 |                             "type": "string",
1277 |                             "description": "The issue key (e.g., PROJECT-123)",
1278 |                         },
1279 |                         "comment": {
1280 |                             "type": "string",
1281 |                             "description": "The comment text",
1282 |                         },
1283 |                     },
1284 |                     "required": ["issue_key", "comment"],
1285 |                 },
1286 |             ),
1287 |             Tool(
1288 |                 name=JiraTools.GET_TRANSITIONS.value,
1289 |                 description="Get available workflow transitions for a Jira issue",
1290 |                 inputSchema={
1291 |                     "type": "object",
1292 |                     "properties": {
1293 |                         "issue_key": {
1294 |                             "type": "string",
1295 |                             "description": "The issue key (e.g., PROJECT-123)",
1296 |                         }
1297 |                     },
1298 |                     "required": ["issue_key"],
1299 |                 },
1300 |             ),
1301 |             Tool(
1302 |                 name=JiraTools.TRANSITION_ISSUE.value,
1303 |                 description="Transition a Jira issue to a new status",
1304 |                 inputSchema={
1305 |                     "type": "object",
1306 |                     "properties": {
1307 |                         "issue_key": {
1308 |                             "type": "string",
1309 |                             "description": "The issue key (e.g., PROJECT-123)",
1310 |                         },
1311 |                         "transition_id": {
1312 |                             "type": "string",
1313 |                             "description": "ID of the transition to perform (get IDs using get_transitions)",
1314 |                         },
1315 |                         "comment": {
1316 |                             "type": "string",
1317 |                             "description": "Comment to add during transition (optional)",
1318 |                         },
1319 |                         "fields": {
1320 |                             "type": "object",
1321 |                             "description": "Additional fields to update during transition (optional)",
1322 |                         },
1323 |                     },
1324 |                     "required": ["issue_key", "transition_id"],
1325 |                 },
1326 |             ),
1327 |             Tool(
1328 |                 name=JiraTools.GET_PROJECT_ISSUE_TYPES.value,
1329 |                 description="Get all available issue types for a specific Jira project",
1330 |                 inputSchema={
1331 |                     "type": "object",
1332 |                     "properties": {
1333 |                         "project_key": {
1334 |                             "type": "string",
1335 |                             "description": "The project key (e.g., 'MYPROJ')",
1336 |                         }
1337 |                     },
1338 |                     "required": ["project_key"],
1339 |                 },
1340 |             ),
1341 |             Tool(
1342 |                 name=JiraTools.CREATE_PROJECT.value,
1343 |                 description="Create a new Jira project using v3 REST API",
1344 |                 inputSchema={
1345 |                     "type": "object",
1346 |                     "properties": {
1347 |                         "key": {
1348 |                             "type": "string",
1349 |                             "description": "Mandatory. Must match Jira project key requirements, usually only 2-10 uppercase characters.",
1350 |                         },
1351 |                         "name": {
1352 |                             "type": "string",
1353 |                             "description": "If not specified it will use the key value.",
1354 |                         },
1355 |                         "assignee": {
1356 |                             "type": "string",
1357 |                             "description": "Lead account ID or username (mapped to leadAccountId in v3 API).",
1358 |                         },
1359 |                         "ptype": {
1360 |                             "type": "string",
1361 |                             "description": "Project type key: 'software', 'business', or 'service_desk'. Defaults to 'software'.",
1362 |                         },
1363 |                         "template_name": {
1364 |                             "type": "string",
1365 |                             "description": "Project template key for creating from templates (mapped to projectTemplateKey in v3 API).",
1366 |                         },
1367 |                         "avatarId": {
1368 |                             "type": ["integer", "string"],
1369 |                             "description": "ID of the avatar to use for the project.",
1370 |                         },
1371 |                         "issueSecurityScheme": {
1372 |                             "type": ["integer", "string"],
1373 |                             "description": "Determines the security scheme to use.",
1374 |                         },
1375 |                         "permissionScheme": {
1376 |                             "type": ["integer", "string"],
1377 |                             "description": "Determines the permission scheme to use.",
1378 |                         },
1379 |                         "projectCategory": {
1380 |                             "type": ["integer", "string"],
1381 |                             "description": "Determines the category the project belongs to.",
1382 |                         },
1383 |                         "notificationScheme": {
1384 |                             "type": ["integer", "string"],
1385 |                             "description": "Determines the notification scheme to use. Default is None.",
1386 |                         },
1387 |                         "categoryId": {
1388 |                             "type": ["integer", "string"],
1389 |                             "description": "Same as projectCategory. Can be used interchangeably.",
1390 |                         },
1391 |                         "url": {
1392 |                             "type": "string",
1393 |                             "description": "A link to information about the project, such as documentation.",
1394 |                         },
1395 |                     },
1396 |                     "required": ["key"],
1397 |                 },
1398 |             ),
1399 |         ]
1400 | 
1401 |     @server.call_tool()
1402 |     async def call_tool(
1403 |         name: str, arguments: dict
1404 |     ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
1405 |         """Handle tool calls for Jira operations."""
1406 |         logger.info(f"call_tool invoked. Tool: '{name}', Arguments: {arguments}")
1407 |         try:
1408 |             result: Any
1409 | 
1410 |             match name:
1411 |                 case JiraTools.GET_PROJECTS.value:
1412 |                     logger.info("About to AWAIT jira_server.get_jira_projects...")
1413 |                     result = await jira_server.get_jira_projects()
1414 |                     logger.info(
1415 |                         f"COMPLETED await jira_server.get_jira_projects. Result has {len(result)} items."
1416 |                     )
1417 | 
1418 |                 case JiraTools.GET_ISSUE.value:
1419 |                     logger.info("Calling synchronous tool get_jira_issue...")
1420 |                     issue_key = arguments.get("issue_key")
1421 |                     if not issue_key:
1422 |                         raise ValueError("Missing required argument: issue_key")
1423 |                     result = jira_server.get_jira_issue(issue_key)
1424 |                     logger.info("Synchronous tool get_jira_issue completed.")
1425 | 
1426 |                 case JiraTools.SEARCH_ISSUES.value:
1427 |                     logger.info("Calling async tool search_jira_issues...")
1428 |                     jql = arguments.get("jql")
1429 |                     if not jql:
1430 |                         raise ValueError("Missing required argument: jql")
1431 |                     max_results = arguments.get("max_results", 10)
1432 |                     result = await jira_server.search_jira_issues(jql, max_results)
1433 |                     logger.info("Async tool search_jira_issues completed.")
1434 | 
1435 |                 case JiraTools.CREATE_ISSUE.value:
1436 |                     logger.info("About to AWAIT jira_server.create_jira_issue...")
1437 |                     required_args = ["project", "summary", "description", "issue_type"]
1438 |                     if not all(arg in arguments for arg in required_args):
1439 |                         missing = [arg for arg in required_args if arg not in arguments]
1440 |                         raise ValueError(
1441 |                             f"Missing required arguments: {', '.join(missing)}"
1442 |                         )
1443 |                     result = await jira_server.create_jira_issue(
1444 |                         arguments["project"],
1445 |                         arguments["summary"],
1446 |                         arguments["description"],
1447 |                         arguments["issue_type"],
1448 |                         arguments.get("fields", {}),
1449 |                     )
1450 |                     logger.info("COMPLETED await jira_server.create_jira_issue.")
1451 | 
1452 |                 case JiraTools.CREATE_ISSUES.value:
1453 |                     logger.info("Calling async tool create_jira_issues...")
1454 |                     field_list = arguments.get("field_list")
1455 |                     if not field_list:
1456 |                         raise ValueError("Missing required argument: field_list")
1457 |                     prefetch = arguments.get("prefetch", True)
1458 |                     result = await jira_server.create_jira_issues(field_list, prefetch)
1459 |                     logger.info("Async tool create_jira_issues completed.")
1460 | 
1461 |                 case JiraTools.ADD_COMMENT.value:
1462 |                     logger.info("About to AWAIT jira_server.add_jira_comment...")
1463 |                     issue_key = arguments.get("issue_key")
1464 |                     comment_text = arguments.get("comment") or arguments.get("body")
1465 |                     if not issue_key or not comment_text:
1466 |                         raise ValueError(
1467 |                             "Missing required arguments: issue_key and comment (or body)"
1468 |                         )
1469 |                     result = await jira_server.add_jira_comment(issue_key, comment_text)
1470 |                     logger.info("COMPLETED await jira_server.add_jira_comment.")
1471 | 
1472 |                 case JiraTools.GET_TRANSITIONS.value:
1473 |                     logger.info("About to AWAIT jira_server.get_jira_transitions...")
1474 |                     issue_key = arguments.get("issue_key")
1475 |                     if not issue_key:
1476 |                         raise ValueError("Missing required argument: issue_key")
1477 |                     result = await jira_server.get_jira_transitions(issue_key)
1478 |                     logger.info("COMPLETED await jira_server.get_jira_transitions.")
1479 | 
1480 |                 case JiraTools.TRANSITION_ISSUE.value:
1481 |                     logger.info("Calling async tool transition_jira_issue...")
1482 |                     issue_key = arguments.get("issue_key")
1483 |                     transition_id = arguments.get("transition_id")
1484 |                     if not issue_key or not transition_id:
1485 |                         raise ValueError(
1486 |                             "Missing required arguments: issue_key and transition_id"
1487 |                         )
1488 |                     comment = arguments.get("comment")
1489 |                     fields = arguments.get("fields")
1490 |                     result = await jira_server.transition_jira_issue(
1491 |                         issue_key, transition_id, comment, fields
1492 |                     )
1493 |                     logger.info("Async tool transition_jira_issue completed.")
1494 | 
1495 |                 case JiraTools.GET_PROJECT_ISSUE_TYPES.value:
1496 |                     logger.info(
1497 |                         "Calling asynchronous tool get_jira_project_issue_types..."
1498 |                     )
1499 |                     project_key = arguments.get("project_key")
1500 |                     if not project_key:
1501 |                         raise ValueError("Missing required argument: project_key")
1502 |                     result = await jira_server.get_jira_project_issue_types(project_key)
1503 |                     logger.info(
1504 |                         "Asynchronous tool get_jira_project_issue_types completed."
1505 |                     )
1506 | 
1507 |                 case JiraTools.CREATE_PROJECT.value:
1508 |                     logger.info("About to AWAIT jira_server.create_jira_project...")
1509 |                     key = arguments.get("key")
1510 |                     if not key:
1511 |                         raise ValueError("Missing required argument: key")
1512 |                     # Type conversion logic from original code
1513 |                     for int_key in [
1514 |                         "avatarId",
1515 |                         "issueSecurityScheme",
1516 |                         "permissionScheme",
1517 |                         "projectCategory",
1518 |                         "notificationScheme",
1519 |                         "categoryId",
1520 |                     ]:
1521 |                         if (
1522 |                             int_key in arguments
1523 |                             and isinstance(arguments[int_key], str)
1524 |                             and arguments[int_key].isdigit()
1525 |                         ):
1526 |                             arguments[int_key] = int(arguments[int_key])
1527 |                     result = await jira_server.create_jira_project(**arguments)
1528 |                     logger.info("COMPLETED await jira_server.create_jira_project.")
1529 | 
1530 |                 case _:
1531 |                     raise ValueError(f"Unknown tool: {name}")
1532 | 
1533 |             logger.debug("Serializing result to JSON...")
1534 | 
1535 |             # Handle serialization properly for different result types
1536 |             if isinstance(result, list):
1537 |                 # If it's a list, check each item individually
1538 |                 serialized_result = []
1539 |                 for item in result:
1540 |                     if hasattr(item, "model_dump"):
1541 |                         serialized_result.append(item.model_dump())
1542 |                     else:
1543 |                         # It's already a dict or basic type
1544 |                         serialized_result.append(item)
1545 |             else:
1546 |                 # Single item result
1547 |                 if hasattr(result, "model_dump"):
1548 |                     serialized_result = result.model_dump()
1549 |                 else:
1550 |                     # It's already a dict or basic type
1551 |                     serialized_result = result
1552 | 
1553 |             json_result = json.dumps(serialized_result, indent=2)
1554 |             return [TextContent(type="text", text=json_result)]
1555 | 
1556 |         except Exception as e:
1557 |             logger.critical(
1558 |                 f"FATAL error in call_tool for tool '{name}'", exc_info=True
1559 |             )
1560 |             return [
1561 |                 TextContent(
1562 |                     type="text",
1563 |                     text=json.dumps(
1564 |                         {
1565 |                             "error": f"Error in tool '{name}': {type(e).__name__}: {str(e)}"
1566 |                         }
1567 |                     ),
1568 |                 )
1569 |             ]
1570 | 
1571 |     options = server.create_initialization_options()
1572 |     async with stdio_server() as (read_stream, write_stream):
1573 |         await server.run(read_stream, write_stream, options)
1574 | 
```
Page 2/4FirstPrevNextLast