Skip to content

Commit ca05d51

Browse files
authored
fix(jira): migrate Cloud search to v3 API endpoint (sooperset#769)
The Jira Cloud v2 search API (/rest/api/*/search) has been deprecated and is now returning errors. This commit migrates Cloud deployments to the new v3 search endpoint (POST /rest/api/3/search/jql). Changes: - Cloud: Use POST /rest/api/3/search/jql with nextPageToken pagination - Server/DC: Continue using the v2 /rest/api/2/search endpoint unchanged - Updated tests to mock the new v3 API calls for Cloud scenarios The v3 API has some differences: - Uses JSON body instead of query parameters - Uses nextPageToken for pagination instead of startAt - Does not return total count (returns -1) - Max 100 results per request (was 50 for Server/DC) Fixes sooperset#720 Github-Issue:sooperset#720
1 parent 212f9db commit ca05d51

File tree

2 files changed

+155
-189
lines changed

2 files changed

+155
-189
lines changed

src/mcp_atlassian/jira/search.py

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Module for Jira search operations."""
22

33
import logging
4+
from typing import Any
45

56
import requests
67
from requests.exceptions import HTTPError
@@ -86,55 +87,61 @@ def search_issues(
8687
fields_param = fields
8788

8889
if self.config.is_cloud:
89-
actual_total = -1
90-
try:
91-
# Call 1: Get metadata (including total) using standard search API
92-
metadata_params = {"jql": jql, "maxResults": 0}
93-
metadata_response = self.jira.get(
94-
self.jira.resource_url("search"), params=metadata_params
95-
)
90+
# Cloud: Use v3 API endpoint POST /rest/api/3/search/jql
91+
# The old v2 /rest/api/*/search endpoint is deprecated
92+
# See: https://developer.atlassian.com/changelog/#CHANGE-2046
93+
94+
# Build request body for v3 API
95+
fields_list = fields_param.split(",") if fields_param else ["id", "key"]
96+
request_body: dict[str, Any] = {
97+
"jql": jql,
98+
"maxResults": min(limit, 100), # v3 API max is 100 per request
99+
"fields": fields_list,
100+
}
101+
# Note: v3 API uses 'expand' as a comma-separated string, not an array
102+
if expand:
103+
request_body["expand"] = expand
96104

97-
if (
98-
isinstance(metadata_response, dict)
99-
and "total" in metadata_response
100-
):
101-
try:
102-
actual_total = int(metadata_response["total"])
103-
except (ValueError, TypeError):
104-
logger.warning(
105-
f"Could not parse 'total' from metadata response for JQL: {jql}. Received: {metadata_response.get('total')}"
106-
)
107-
else:
108-
logger.warning(
109-
f"Could not retrieve total count from metadata response for JQL: {jql}. Response type: {type(metadata_response)}"
110-
)
111-
except Exception as meta_err:
112-
logger.error(
113-
f"Error fetching metadata for JQL '{jql}': {str(meta_err)}"
114-
)
105+
# Fetch issues using v3 API with nextPageToken pagination
106+
all_issues: list[dict[str, Any]] = []
107+
next_page_token: str | None = None
115108

116-
# Call 2: Get the actual issues using the enhanced method
117-
issues_response_list = self.jira.enhanced_jql_get_list_of_tickets(
118-
jql, fields=fields_param, limit=limit, expand=expand
119-
)
109+
while len(all_issues) < limit:
110+
if next_page_token:
111+
request_body["nextPageToken"] = next_page_token
120112

121-
if not isinstance(issues_response_list, list):
122-
msg = f"Unexpected return value type from `jira.enhanced_jql_get_list_of_tickets`: {type(issues_response_list)}"
123-
logger.error(msg)
124-
raise TypeError(msg)
113+
response = self.jira.post(
114+
"rest/api/3/search/jql", json=request_body
115+
)
125116

126-
response_dict_for_model = {
127-
"issues": issues_response_list,
128-
"total": actual_total,
117+
if not isinstance(response, dict):
118+
msg = f"Unexpected response type from v3 search API: {type(response)}"
119+
logger.error(msg)
120+
raise TypeError(msg)
121+
122+
issues = response.get("issues", [])
123+
all_issues.extend(issues)
124+
125+
# Check for more pages
126+
next_page_token = response.get("nextPageToken")
127+
if not next_page_token:
128+
break
129+
130+
# Build response dict for model
131+
# Note: v3 API doesn't provide total count, so we use -1
132+
response_dict: dict[str, Any] = {
133+
"issues": all_issues[:limit],
134+
"total": -1,
135+
"startAt": 0,
136+
"maxResults": limit,
129137
}
130138

131139
search_result = JiraSearchResult.from_api_response(
132-
response_dict_for_model,
140+
response_dict,
133141
base_url=self.config.url,
134142
requested_fields=fields_param,
135143
)
136144

137-
# Return the full search result object
138145
return search_result
139146
else:
140147
limit = min(limit, 50)

0 commit comments

Comments
 (0)