datarobot-genai 0.2.14__py3-none-any.whl → 0.2.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datarobot_genai/drmcp/tools/clients/confluence.py +113 -4
- datarobot_genai/drmcp/tools/clients/jira.py +119 -5
- datarobot_genai/drmcp/tools/confluence/tools.py +61 -2
- datarobot_genai/drmcp/tools/jira/tools.py +142 -0
- {datarobot_genai-0.2.14.dist-info → datarobot_genai-0.2.16.dist-info}/METADATA +1 -1
- {datarobot_genai-0.2.14.dist-info → datarobot_genai-0.2.16.dist-info}/RECORD +10 -10
- {datarobot_genai-0.2.14.dist-info → datarobot_genai-0.2.16.dist-info}/WHEEL +0 -0
- {datarobot_genai-0.2.14.dist-info → datarobot_genai-0.2.16.dist-info}/entry_points.txt +0 -0
- {datarobot_genai-0.2.14.dist-info → datarobot_genai-0.2.16.dist-info}/licenses/AUTHORS +0 -0
- {datarobot_genai-0.2.14.dist-info → datarobot_genai-0.2.16.dist-info}/licenses/LICENSE +0 -0
|
@@ -31,6 +31,14 @@ from .atlassian import get_atlassian_cloud_id
|
|
|
31
31
|
logger = logging.getLogger(__name__)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class ConfluenceError(Exception):
|
|
35
|
+
"""Exception for Confluence API errors."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.status_code = status_code
|
|
40
|
+
|
|
41
|
+
|
|
34
42
|
class ConfluencePage(BaseModel):
|
|
35
43
|
"""Pydantic model for Confluence page."""
|
|
36
44
|
|
|
@@ -133,7 +141,7 @@ class ConfluenceClient:
|
|
|
133
141
|
|
|
134
142
|
Raises
|
|
135
143
|
------
|
|
136
|
-
|
|
144
|
+
ConfluenceError: If page is not found
|
|
137
145
|
httpx.HTTPStatusError: If the API request fails
|
|
138
146
|
"""
|
|
139
147
|
cloud_id = await self._get_cloud_id()
|
|
@@ -142,7 +150,7 @@ class ConfluenceClient:
|
|
|
142
150
|
response = await self._client.get(url, params={"expand": self.EXPAND_FIELDS})
|
|
143
151
|
|
|
144
152
|
if response.status_code == HTTPStatus.NOT_FOUND:
|
|
145
|
-
raise
|
|
153
|
+
raise ConfluenceError(f"Page with ID '{page_id}' not found", status_code=404)
|
|
146
154
|
|
|
147
155
|
response.raise_for_status()
|
|
148
156
|
return self._parse_response(response.json())
|
|
@@ -161,7 +169,7 @@ class ConfluenceClient:
|
|
|
161
169
|
|
|
162
170
|
Raises
|
|
163
171
|
------
|
|
164
|
-
|
|
172
|
+
ConfluenceError: If the page is not found
|
|
165
173
|
httpx.HTTPStatusError: If the API request fails
|
|
166
174
|
"""
|
|
167
175
|
cloud_id = await self._get_cloud_id()
|
|
@@ -181,10 +189,111 @@ class ConfluenceClient:
|
|
|
181
189
|
results = data.get("results", [])
|
|
182
190
|
|
|
183
191
|
if not results:
|
|
184
|
-
raise
|
|
192
|
+
raise ConfluenceError(
|
|
193
|
+
f"Page with title '{title}' not found in space '{space_key}'", status_code=404
|
|
194
|
+
)
|
|
185
195
|
|
|
186
196
|
return self._parse_response(results[0])
|
|
187
197
|
|
|
198
|
+
def _extract_error_message(self, response: httpx.Response) -> str:
|
|
199
|
+
"""Extract error message from Confluence API error response."""
|
|
200
|
+
try:
|
|
201
|
+
error_data = response.json()
|
|
202
|
+
# Confluence API returns errors in different formats
|
|
203
|
+
if "message" in error_data:
|
|
204
|
+
return error_data["message"]
|
|
205
|
+
if "errorMessages" in error_data and error_data["errorMessages"]:
|
|
206
|
+
return "; ".join(error_data["errorMessages"])
|
|
207
|
+
if "errors" in error_data:
|
|
208
|
+
errors = error_data["errors"]
|
|
209
|
+
if isinstance(errors, list):
|
|
210
|
+
return "; ".join(str(e) for e in errors)
|
|
211
|
+
if isinstance(errors, dict):
|
|
212
|
+
return "; ".join(f"{k}: {v}" for k, v in errors.items())
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
return response.text or "Unknown error"
|
|
216
|
+
|
|
217
|
+
async def create_page(
|
|
218
|
+
self,
|
|
219
|
+
space_key: str,
|
|
220
|
+
title: str,
|
|
221
|
+
body_content: str,
|
|
222
|
+
parent_id: int | None = None,
|
|
223
|
+
) -> ConfluencePage:
|
|
224
|
+
"""
|
|
225
|
+
Create a new Confluence page in a specified space.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
space_key: The key of the Confluence space where the page should live
|
|
229
|
+
title: The title of the new page
|
|
230
|
+
body_content: The content in Confluence Storage Format (XML) or raw text
|
|
231
|
+
parent_id: Optional ID of the parent page for creating a child page
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
ConfluencePage with the created page data
|
|
236
|
+
|
|
237
|
+
Raises
|
|
238
|
+
------
|
|
239
|
+
ConfluenceError: If space not found, parent page not found, duplicate title,
|
|
240
|
+
permission denied, or invalid content
|
|
241
|
+
httpx.HTTPStatusError: If the API request fails with unexpected status
|
|
242
|
+
"""
|
|
243
|
+
cloud_id = await self._get_cloud_id()
|
|
244
|
+
url = f"{ATLASSIAN_API_BASE}/ex/confluence/{cloud_id}/wiki/rest/api/content"
|
|
245
|
+
|
|
246
|
+
payload: dict[str, Any] = {
|
|
247
|
+
"type": "page",
|
|
248
|
+
"title": title,
|
|
249
|
+
"space": {"key": space_key},
|
|
250
|
+
"body": {
|
|
251
|
+
"storage": {
|
|
252
|
+
"value": body_content,
|
|
253
|
+
"representation": "storage",
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if parent_id is not None:
|
|
259
|
+
payload["ancestors"] = [{"id": parent_id}]
|
|
260
|
+
|
|
261
|
+
response = await self._client.post(url, json=payload)
|
|
262
|
+
|
|
263
|
+
if response.status_code == HTTPStatus.NOT_FOUND:
|
|
264
|
+
error_msg = self._extract_error_message(response)
|
|
265
|
+
if parent_id is not None and "ancestor" in error_msg.lower():
|
|
266
|
+
raise ConfluenceError(
|
|
267
|
+
f"Parent page with ID '{parent_id}' not found", status_code=404
|
|
268
|
+
)
|
|
269
|
+
raise ConfluenceError(
|
|
270
|
+
f"Space '{space_key}' not found or resource unavailable: {error_msg}",
|
|
271
|
+
status_code=404,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if response.status_code == HTTPStatus.CONFLICT:
|
|
275
|
+
raise ConfluenceError(
|
|
276
|
+
f"A page with title '{title}' already exists in space '{space_key}'",
|
|
277
|
+
status_code=409,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if response.status_code == HTTPStatus.FORBIDDEN:
|
|
281
|
+
raise ConfluenceError(
|
|
282
|
+
f"Permission denied: you don't have access to create pages in space '{space_key}'",
|
|
283
|
+
status_code=403,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if response.status_code == HTTPStatus.BAD_REQUEST:
|
|
287
|
+
error_msg = self._extract_error_message(response)
|
|
288
|
+
raise ConfluenceError(f"Invalid request: {error_msg}", status_code=400)
|
|
289
|
+
|
|
290
|
+
if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
|
|
291
|
+
raise ConfluenceError("Rate limit exceeded. Please try again later.", status_code=429)
|
|
292
|
+
|
|
293
|
+
response.raise_for_status()
|
|
294
|
+
|
|
295
|
+
return self._parse_response(response.json())
|
|
296
|
+
|
|
188
297
|
async def __aenter__(self) -> "ConfluenceClient":
|
|
189
298
|
"""Async context manager entry."""
|
|
190
299
|
return self
|
|
@@ -25,6 +25,18 @@ from .atlassian import get_atlassian_cloud_id
|
|
|
25
25
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
|
+
RESPONSE_JIRA_ISSUE_FIELDS = {
|
|
29
|
+
"id",
|
|
30
|
+
"key",
|
|
31
|
+
"summary",
|
|
32
|
+
"status",
|
|
33
|
+
"reporter",
|
|
34
|
+
"assignee",
|
|
35
|
+
"created",
|
|
36
|
+
"updated",
|
|
37
|
+
}
|
|
38
|
+
RESPONSE_JIRA_ISSUE_FIELDS_STR = ",".join(RESPONSE_JIRA_ISSUE_FIELDS)
|
|
39
|
+
|
|
28
40
|
|
|
29
41
|
class _IssuePerson(BaseModel):
|
|
30
42
|
email_address: str = Field(alias="emailAddress")
|
|
@@ -107,10 +119,41 @@ class JiraClient:
|
|
|
107
119
|
self._cloud_id = await get_atlassian_cloud_id(self._client, service_type="jira")
|
|
108
120
|
return self._cloud_id
|
|
109
121
|
|
|
110
|
-
async def _get_full_url(self,
|
|
122
|
+
async def _get_full_url(self, path: str) -> str:
|
|
111
123
|
"""Return URL for Jira API."""
|
|
112
124
|
cloud_id = await self._get_cloud_id()
|
|
113
|
-
return f"{ATLASSIAN_API_BASE}/ex/jira/{cloud_id}/rest/api/3/{
|
|
125
|
+
return f"{ATLASSIAN_API_BASE}/ex/jira/{cloud_id}/rest/api/3/{path}"
|
|
126
|
+
|
|
127
|
+
async def search_jira_issues(self, jql_query: str, max_results: int) -> list[Issue]:
|
|
128
|
+
"""
|
|
129
|
+
Search Jira issues using JQL (Jira Query Language).
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
jql_query: JQL Query
|
|
133
|
+
max_results: Maximum number of issues to return
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
List of Jira issues
|
|
138
|
+
|
|
139
|
+
Raises
|
|
140
|
+
------
|
|
141
|
+
httpx.HTTPStatusError: If the API request fails
|
|
142
|
+
"""
|
|
143
|
+
url = await self._get_full_url("search/jql")
|
|
144
|
+
response = await self._client.post(
|
|
145
|
+
url,
|
|
146
|
+
json={
|
|
147
|
+
"jql": jql_query,
|
|
148
|
+
"fields": list(RESPONSE_JIRA_ISSUE_FIELDS),
|
|
149
|
+
"maxResults": max_results,
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
response.raise_for_status()
|
|
154
|
+
raw_issues = response.json().get("issues", [])
|
|
155
|
+
issues = [Issue(**issue) for issue in raw_issues]
|
|
156
|
+
return issues
|
|
114
157
|
|
|
115
158
|
async def get_jira_issue(self, issue_key: str) -> Issue:
|
|
116
159
|
"""
|
|
@@ -128,9 +171,7 @@ class JiraClient:
|
|
|
128
171
|
httpx.HTTPStatusError: If the API request fails
|
|
129
172
|
"""
|
|
130
173
|
url = await self._get_full_url(f"issue/{issue_key}")
|
|
131
|
-
response = await self._client.get(
|
|
132
|
-
url, params={"fields": "id,key,summary,status,reporter,assignee,created,updated"}
|
|
133
|
-
)
|
|
174
|
+
response = await self._client.get(url, params={"fields": RESPONSE_JIRA_ISSUE_FIELDS_STR})
|
|
134
175
|
|
|
135
176
|
if response.status_code == HTTPStatus.NOT_FOUND:
|
|
136
177
|
raise ValueError(f"{issue_key} not found")
|
|
@@ -209,6 +250,79 @@ class JiraClient:
|
|
|
209
250
|
jsoned = response.json()
|
|
210
251
|
return jsoned["key"]
|
|
211
252
|
|
|
253
|
+
async def update_jira_issue(self, issue_key: str, fields: dict[str, Any]) -> list[str]:
|
|
254
|
+
"""
|
|
255
|
+
Update Jira issue.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
issue_key: The key of the Jira issue, e.g., 'PROJ-123'
|
|
259
|
+
fields: A dictionary of field names and their new values
|
|
260
|
+
e.g., {'description': 'New content'}
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
List of updated fields
|
|
265
|
+
|
|
266
|
+
Raises
|
|
267
|
+
------
|
|
268
|
+
httpx.HTTPStatusError: If the API request fails
|
|
269
|
+
"""
|
|
270
|
+
url = await self._get_full_url(f"issue/{issue_key}")
|
|
271
|
+
payload = {"fields": fields}
|
|
272
|
+
|
|
273
|
+
response = await self._client.put(url, json=payload)
|
|
274
|
+
|
|
275
|
+
response.raise_for_status()
|
|
276
|
+
return list(fields.keys())
|
|
277
|
+
|
|
278
|
+
async def get_available_jira_transitions(self, issue_key: str) -> dict[str, str]:
|
|
279
|
+
"""
|
|
280
|
+
Get Available Jira Transitions.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
issue_key: The key of the Jira issue, e.g., 'PROJ-123'
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
Dictionary where key is the transition name and value is the transition ID
|
|
288
|
+
|
|
289
|
+
Raises
|
|
290
|
+
------
|
|
291
|
+
httpx.HTTPStatusError: If the API request fails
|
|
292
|
+
"""
|
|
293
|
+
url = await self._get_full_url(f"issue/{issue_key}/transitions")
|
|
294
|
+
response = await self._client.get(url)
|
|
295
|
+
response.raise_for_status()
|
|
296
|
+
jsoned = response.json()
|
|
297
|
+
transitions = {
|
|
298
|
+
transition["name"]: transition["id"] for transition in jsoned.get("transitions", [])
|
|
299
|
+
}
|
|
300
|
+
return transitions
|
|
301
|
+
|
|
302
|
+
async def transition_jira_issue(self, issue_key: str, transition_id: str) -> None:
|
|
303
|
+
"""
|
|
304
|
+
Transition Jira issue.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
issue_key: The key of the Jira issue, e.g., 'PROJ-123'
|
|
308
|
+
transition_id: Id of target transitionm e.g. '123'.
|
|
309
|
+
Can be obtained from `get_available_jira_transitions`.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
Nothing
|
|
314
|
+
|
|
315
|
+
Raises
|
|
316
|
+
------
|
|
317
|
+
httpx.HTTPStatusError: If the API request fails
|
|
318
|
+
"""
|
|
319
|
+
url = await self._get_full_url(f"issue/{issue_key}")
|
|
320
|
+
payload = {"transition": {"id": transition_id}}
|
|
321
|
+
|
|
322
|
+
response = await self._client.post(url, json=payload)
|
|
323
|
+
|
|
324
|
+
response.raise_for_status()
|
|
325
|
+
|
|
212
326
|
async def __aenter__(self) -> "JiraClient":
|
|
213
327
|
"""Async context manager entry."""
|
|
214
328
|
return self
|
|
@@ -23,6 +23,7 @@ from fastmcp.tools.tool import ToolResult
|
|
|
23
23
|
from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
|
|
24
24
|
from datarobot_genai.drmcp.tools.clients.atlassian import get_atlassian_access_token
|
|
25
25
|
from datarobot_genai.drmcp.tools.clients.confluence import ConfluenceClient
|
|
26
|
+
from datarobot_genai.drmcp.tools.clients.confluence import ConfluenceError
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -65,8 +66,8 @@ async def confluence_get_page(
|
|
|
65
66
|
"'space_key' is required when identifying a page by title."
|
|
66
67
|
)
|
|
67
68
|
page_response = await client.get_page_by_title(page_id_or_title, space_key)
|
|
68
|
-
except
|
|
69
|
-
logger.error(f"
|
|
69
|
+
except ConfluenceError as e:
|
|
70
|
+
logger.error(f"Confluence error getting page: {e}")
|
|
70
71
|
raise ToolError(str(e))
|
|
71
72
|
except Exception as e:
|
|
72
73
|
logger.error(f"Unexpected error getting Confluence page: {e}")
|
|
@@ -79,3 +80,61 @@ async def confluence_get_page(
|
|
|
79
80
|
content=f"Successfully retrieved page '{page_response.title}'.",
|
|
80
81
|
structured_content=page_response.as_flat_dict(),
|
|
81
82
|
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dr_mcp_tool(tags={"confluence", "write", "create", "page"})
|
|
86
|
+
async def confluence_create_page(
|
|
87
|
+
*,
|
|
88
|
+
space_key: Annotated[str, "The key of the Confluence space where the new page should live."],
|
|
89
|
+
title: Annotated[str, "The title of the new page."],
|
|
90
|
+
body_content: Annotated[
|
|
91
|
+
str,
|
|
92
|
+
"The content of the page, typically in Confluence Storage Format (XML) or raw text.",
|
|
93
|
+
],
|
|
94
|
+
parent_id: Annotated[
|
|
95
|
+
int | None,
|
|
96
|
+
"The ID of the parent page, used to create a child page.",
|
|
97
|
+
] = None,
|
|
98
|
+
) -> ToolResult:
|
|
99
|
+
"""Create a new documentation page in a specified Confluence space.
|
|
100
|
+
|
|
101
|
+
Use this tool to create new Confluence pages with content in storage format.
|
|
102
|
+
The page will be created at the root level of the space unless a parent_id
|
|
103
|
+
is provided, in which case it will be created as a child page.
|
|
104
|
+
|
|
105
|
+
Usage:
|
|
106
|
+
- Root page: space_key="PROJ", title="New Page", body_content="<p>Content</p>"
|
|
107
|
+
- Child page: space_key="PROJ", title="Sub Page", body_content="<p>Content</p>",
|
|
108
|
+
parent_id=123456
|
|
109
|
+
"""
|
|
110
|
+
if not all([space_key, title, body_content]):
|
|
111
|
+
raise ToolError(
|
|
112
|
+
"Argument validation error: space_key, title, and body_content are required fields."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
access_token = await get_atlassian_access_token()
|
|
116
|
+
if isinstance(access_token, ToolError):
|
|
117
|
+
raise access_token
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
async with ConfluenceClient(access_token) as client:
|
|
121
|
+
page_response = await client.create_page(
|
|
122
|
+
space_key=space_key,
|
|
123
|
+
title=title,
|
|
124
|
+
body_content=body_content,
|
|
125
|
+
parent_id=parent_id,
|
|
126
|
+
)
|
|
127
|
+
except ConfluenceError as e:
|
|
128
|
+
logger.error(f"Confluence error creating page: {e}")
|
|
129
|
+
raise ToolError(str(e))
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Unexpected error creating Confluence page: {e}")
|
|
132
|
+
raise ToolError(
|
|
133
|
+
f"An unexpected error occurred while creating Confluence page "
|
|
134
|
+
f"'{title}' in space '{space_key}': {str(e)}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return ToolResult(
|
|
138
|
+
content=f"New page '{title}' created successfully in space '{space_key}'.",
|
|
139
|
+
structured_content={"new_page_id": page_response.page_id, "title": page_response.title},
|
|
140
|
+
)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
16
|
from typing import Annotated
|
|
17
|
+
from typing import Any
|
|
17
18
|
|
|
18
19
|
from fastmcp.exceptions import ToolError
|
|
19
20
|
from fastmcp.tools.tool import ToolResult
|
|
@@ -25,6 +26,40 @@ from datarobot_genai.drmcp.tools.clients.jira import JiraClient
|
|
|
25
26
|
logger = logging.getLogger(__name__)
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
@dr_mcp_tool(tags={"jira", "search", "issues"})
|
|
30
|
+
async def jira_search_issues(
|
|
31
|
+
*,
|
|
32
|
+
jql_query: Annotated[
|
|
33
|
+
str, "The JQL (Jira Query Language) string used to filter and search for issues."
|
|
34
|
+
],
|
|
35
|
+
max_results: Annotated[int, "Maximum number of issues to return. Default is 50."] = 50,
|
|
36
|
+
) -> ToolResult:
|
|
37
|
+
"""
|
|
38
|
+
Search for Jira issues using a powerful JQL query string.
|
|
39
|
+
|
|
40
|
+
Refer to JQL documentation for advanced query construction:
|
|
41
|
+
JQL functions: https://support.atlassian.com/jira-service-management-cloud/docs/jql-functions/
|
|
42
|
+
JQL fields: https://support.atlassian.com/jira-service-management-cloud/docs/jql-fields/
|
|
43
|
+
JQL keywords: https://support.atlassian.com/jira-service-management-cloud/docs/use-advanced-search-with-jira-query-language-jql/
|
|
44
|
+
JQL operators: https://support.atlassian.com/jira-service-management-cloud/docs/jql-operators/
|
|
45
|
+
"""
|
|
46
|
+
if not jql_query:
|
|
47
|
+
raise ToolError("Argument validation error: 'jql_query' cannot be empty.")
|
|
48
|
+
|
|
49
|
+
access_token = await get_atlassian_access_token()
|
|
50
|
+
if isinstance(access_token, ToolError):
|
|
51
|
+
raise access_token
|
|
52
|
+
|
|
53
|
+
async with JiraClient(access_token) as client:
|
|
54
|
+
issues = await client.search_jira_issues(jql_query=jql_query, max_results=max_results)
|
|
55
|
+
|
|
56
|
+
n = len(issues)
|
|
57
|
+
return ToolResult(
|
|
58
|
+
content=f"Successfully executed JQL query and retrieved {n} issue(s).",
|
|
59
|
+
structured_content={"data": [issue.as_flat_dict() for issue in issues], "count": n},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
28
63
|
@dr_mcp_tool(tags={"jira", "read", "get", "issue"})
|
|
29
64
|
async def jira_get_issue(
|
|
30
65
|
*, issue_key: Annotated[str, "The key (ID) of the Jira issue to retrieve, e.g., 'PROJ-123'."]
|
|
@@ -99,3 +134,110 @@ async def jira_create_issue(
|
|
|
99
134
|
content=f"Successfully created issue '{issue_key}'.",
|
|
100
135
|
structured_content={"newIssueKey": issue_key, "projectKey": project_key},
|
|
101
136
|
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dr_mcp_tool(tags={"jira", "update", "edit", "issue"})
|
|
140
|
+
async def jira_update_issue(
|
|
141
|
+
*,
|
|
142
|
+
issue_key: Annotated[str, "The key (ID) of the Jira issue to retrieve, e.g., 'PROJ-123'."],
|
|
143
|
+
fields_to_update: Annotated[
|
|
144
|
+
dict[str, Any],
|
|
145
|
+
"A dictionary of field names and their new values (e.g., {'summary': 'New content'}).",
|
|
146
|
+
],
|
|
147
|
+
) -> ToolResult:
|
|
148
|
+
"""
|
|
149
|
+
Modify descriptive fields or custom fields on an existing Jira issue using its key.
|
|
150
|
+
If you want to update issue status you should use `jira_transition_issue` tool instead.
|
|
151
|
+
|
|
152
|
+
Some fields needs very specific schema to allow update.
|
|
153
|
+
You should follow jira rest api guidance.
|
|
154
|
+
Good example is description field:
|
|
155
|
+
"description": {
|
|
156
|
+
"type": "text",
|
|
157
|
+
"version": 1,
|
|
158
|
+
"text": [
|
|
159
|
+
{
|
|
160
|
+
"type": "paragraph",
|
|
161
|
+
"content": [
|
|
162
|
+
{
|
|
163
|
+
"type": "text",
|
|
164
|
+
"text": "[HERE YOU PUT REAL DESCRIPTION]"
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
"""
|
|
171
|
+
if not issue_key:
|
|
172
|
+
raise ToolError("Argument validation error: 'issue_key' cannot be empty.")
|
|
173
|
+
if not fields_to_update or not isinstance(fields_to_update, dict):
|
|
174
|
+
raise ToolError(
|
|
175
|
+
"Argument validation error: 'fields_to_update' must be a non-empty dictionary."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
access_token = await get_atlassian_access_token()
|
|
179
|
+
if isinstance(access_token, ToolError):
|
|
180
|
+
raise access_token
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
async with JiraClient(access_token) as client:
|
|
184
|
+
updated_fields = await client.update_jira_issue(
|
|
185
|
+
issue_key=issue_key, fields=fields_to_update
|
|
186
|
+
)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"Unexpected error while updating Jira issue: {e}")
|
|
189
|
+
raise ToolError(f"An unexpected error occurred while updating Jira issue: {str(e)}")
|
|
190
|
+
|
|
191
|
+
updated_fields_str = ",".join(updated_fields)
|
|
192
|
+
return ToolResult(
|
|
193
|
+
content=f"Successfully updated issue '{issue_key}'. Fields modified: {updated_fields_str}.",
|
|
194
|
+
structured_content={"updatedIssueKey": issue_key, "fields": updated_fields},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dr_mcp_tool(tags={"jira", "update", "transition", "issue"})
|
|
199
|
+
async def jira_transition_issue(
|
|
200
|
+
*,
|
|
201
|
+
issue_key: Annotated[str, "The key (ID) of the Jira issue to transition, e.g. 'PROJ-123'."],
|
|
202
|
+
transition_name: Annotated[
|
|
203
|
+
str, "The exact name of the target status/transition (e.g., 'In Progress')."
|
|
204
|
+
],
|
|
205
|
+
) -> ToolResult:
|
|
206
|
+
"""
|
|
207
|
+
Move a Jira issue through its defined workflow to a new status.
|
|
208
|
+
This leverages Jira's workflow engine directly.
|
|
209
|
+
"""
|
|
210
|
+
if not all([issue_key, transition_name]):
|
|
211
|
+
raise ToolError("Argument validation error: issue_key and transition name/ID are required.")
|
|
212
|
+
|
|
213
|
+
access_token = await get_atlassian_access_token()
|
|
214
|
+
if isinstance(access_token, ToolError):
|
|
215
|
+
raise access_token
|
|
216
|
+
|
|
217
|
+
async with JiraClient(access_token) as client:
|
|
218
|
+
available_transitions = await client.get_available_jira_transitions(issue_key=issue_key)
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
transition_id = available_transitions[transition_name]
|
|
222
|
+
except KeyError:
|
|
223
|
+
available_transitions_str = ",".join(available_transitions)
|
|
224
|
+
raise ToolError(
|
|
225
|
+
f"Unexpected transition name `{transition_name}`. "
|
|
226
|
+
f"Possible values are {available_transitions_str}."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
async with JiraClient(access_token) as client:
|
|
231
|
+
await client.transition_jira_issue(issue_key=issue_key, transition_id=transition_id)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Unexpected error while transitioning Jira issue: {e}")
|
|
234
|
+
raise ToolError(f"An unexpected error occurred while transitioning Jira issue: {str(e)}")
|
|
235
|
+
|
|
236
|
+
return ToolResult(
|
|
237
|
+
content=f"Successfully transitioned issue '{issue_key}' to status '{transition_name}'.",
|
|
238
|
+
structured_content={
|
|
239
|
+
"transitionedIssueKey": issue_key,
|
|
240
|
+
"newStatusName": transition_name,
|
|
241
|
+
"newStatusId": transition_id,
|
|
242
|
+
},
|
|
243
|
+
)
|
|
@@ -76,13 +76,13 @@ datarobot_genai/drmcp/test_utils/utils.py,sha256=esGKFv8aO31-Qg3owayeWp32BYe1CdY
|
|
|
76
76
|
datarobot_genai/drmcp/tools/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
77
77
|
datarobot_genai/drmcp/tools/clients/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
78
78
|
datarobot_genai/drmcp/tools/clients/atlassian.py,sha256=__M_uz7FrcbKCYRzeMn24DCEYD6OmFx_LuywHCxgXsA,6472
|
|
79
|
-
datarobot_genai/drmcp/tools/clients/confluence.py,sha256=
|
|
80
|
-
datarobot_genai/drmcp/tools/clients/jira.py,sha256=
|
|
79
|
+
datarobot_genai/drmcp/tools/clients/confluence.py,sha256=DF6TIGJfR3Lh-D_x66cDNkvOTS8gxL6bVhHRtcP0LKw,10493
|
|
80
|
+
datarobot_genai/drmcp/tools/clients/jira.py,sha256=Rm91JAyrNIqxu66-9rU1YqoRXVnWbEy-Ahvy6f6HlVg,9823
|
|
81
81
|
datarobot_genai/drmcp/tools/clients/s3.py,sha256=GmwzvurFdNfvxOooA8g5S4osRysHYU0S9ypg_177Glg,953
|
|
82
82
|
datarobot_genai/drmcp/tools/confluence/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
83
|
-
datarobot_genai/drmcp/tools/confluence/tools.py,sha256=
|
|
83
|
+
datarobot_genai/drmcp/tools/confluence/tools.py,sha256=iqX7CR57WCXsQxHQCsfPL_Q78QjN9YZv3uIQbTMfYAg,5459
|
|
84
84
|
datarobot_genai/drmcp/tools/jira/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
85
|
-
datarobot_genai/drmcp/tools/jira/tools.py,sha256=
|
|
85
|
+
datarobot_genai/drmcp/tools/jira/tools.py,sha256=dfkqTU2HH-7n44hX80ODFacKq0p0LOchFcZtIIKFNMM,9687
|
|
86
86
|
datarobot_genai/drmcp/tools/predictive/__init__.py,sha256=WuOHlNNEpEmcF7gVnhckruJRKU2qtmJLE3E7zoCGLDo,1030
|
|
87
87
|
datarobot_genai/drmcp/tools/predictive/data.py,sha256=k4EJxJrl8DYVGVfJ0DM4YTfnZlC_K3OUHZ0eRUzfluI,3165
|
|
88
88
|
datarobot_genai/drmcp/tools/predictive/deployment.py,sha256=lm02Ayuo11L1hP41fgi3QpR1Eyty-Wc16rM0c8SgliM,3277
|
|
@@ -106,9 +106,9 @@ datarobot_genai/nat/datarobot_llm_clients.py,sha256=Yu208Ed_p_4P3HdpuM7fYnKcXtim
|
|
|
106
106
|
datarobot_genai/nat/datarobot_llm_providers.py,sha256=aDoQcTeGI-odqydPXEX9OGGNFbzAtpqzTvHHEkmJuEQ,4963
|
|
107
107
|
datarobot_genai/nat/datarobot_mcp_client.py,sha256=35FzilxNp4VqwBYI0NsOc91-xZm1C-AzWqrOdDy962A,9612
|
|
108
108
|
datarobot_genai/nat/helpers.py,sha256=Q7E3ADZdtFfS8E6OQPyw2wgA6laQ58N3bhLj5CBWwJs,3265
|
|
109
|
-
datarobot_genai-0.2.
|
|
110
|
-
datarobot_genai-0.2.
|
|
111
|
-
datarobot_genai-0.2.
|
|
112
|
-
datarobot_genai-0.2.
|
|
113
|
-
datarobot_genai-0.2.
|
|
114
|
-
datarobot_genai-0.2.
|
|
109
|
+
datarobot_genai-0.2.16.dist-info/METADATA,sha256=lTI4a5PfXD7nIUcmAmzENdRiaIfgdaLoruSwq95NYU8,6301
|
|
110
|
+
datarobot_genai-0.2.16.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
111
|
+
datarobot_genai-0.2.16.dist-info/entry_points.txt,sha256=jEW3WxDZ8XIK9-ISmTyt5DbmBb047rFlzQuhY09rGrM,284
|
|
112
|
+
datarobot_genai-0.2.16.dist-info/licenses/AUTHORS,sha256=isJGUXdjq1U7XZ_B_9AH8Qf0u4eX0XyQifJZ_Sxm4sA,80
|
|
113
|
+
datarobot_genai-0.2.16.dist-info/licenses/LICENSE,sha256=U2_VkLIktQoa60Nf6Tbt7E4RMlfhFSjWjcJJfVC-YCE,11341
|
|
114
|
+
datarobot_genai-0.2.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|