backlog-mcp 1.0.3__py3-none-any.whl → 1.0.5__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.
- app/main.py +1 -0
- app/tools/__init__.py +1 -0
- app/tools/get_issue_details.py +4 -1
- app/tools/update_issue_description.py +31 -0
- app/utils/ultils.py +91 -29
- {backlog_mcp-1.0.3.dist-info → backlog_mcp-1.0.5.dist-info}/METADATA +1 -1
- {backlog_mcp-1.0.3.dist-info → backlog_mcp-1.0.5.dist-info}/RECORD +9 -8
- {backlog_mcp-1.0.3.dist-info → backlog_mcp-1.0.5.dist-info}/WHEEL +0 -0
- {backlog_mcp-1.0.3.dist-info → backlog_mcp-1.0.5.dist-info}/entry_points.txt +0 -0
app/main.py
CHANGED
app/tools/__init__.py
CHANGED
app/tools/get_issue_details.py
CHANGED
|
@@ -4,13 +4,15 @@ from app.utils.ultils import get_issue_detail_handler
|
|
|
4
4
|
|
|
5
5
|
async def get_issue_details(
|
|
6
6
|
issue_key: str,
|
|
7
|
-
|
|
7
|
+
include_comments: bool,
|
|
8
|
+
timezone: str = "UTC",
|
|
8
9
|
):
|
|
9
10
|
"""
|
|
10
11
|
Get details of a Backlog issue by its key.
|
|
11
12
|
|
|
12
13
|
Args:
|
|
13
14
|
issue_key (str): The key of the Backlog issue to retrieve.
|
|
15
|
+
include_comments (bool): Whether to include comments in the response.
|
|
14
16
|
timezone (str, optional): The timezone to format datetime fields. Defaults to "UTC".
|
|
15
17
|
"""
|
|
16
18
|
try:
|
|
@@ -22,6 +24,7 @@ async def get_issue_details(
|
|
|
22
24
|
api_key=ctx.api_key,
|
|
23
25
|
issue_key=issue_key,
|
|
24
26
|
timezone=timezone,
|
|
27
|
+
include_comments=include_comments,
|
|
25
28
|
)
|
|
26
29
|
return result
|
|
27
30
|
except Exception as e:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from app.utils.di import create_backlog_context
|
|
2
|
+
from app.utils.ultils import update_issue_description_handler
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
async def update_issue_description(
|
|
6
|
+
issue_key: str,
|
|
7
|
+
description: str,
|
|
8
|
+
):
|
|
9
|
+
"""
|
|
10
|
+
Update the description of a Backlog issue.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
issue_key (str): The key or ID of the Backlog issue to update.
|
|
14
|
+
description (str): The new description content for the issue.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
if not issue_key:
|
|
18
|
+
raise ValueError("Please provide an issue key.")
|
|
19
|
+
if not description:
|
|
20
|
+
raise ValueError("Please provide a description.")
|
|
21
|
+
|
|
22
|
+
ctx = create_backlog_context()
|
|
23
|
+
result = await update_issue_description_handler(
|
|
24
|
+
backlog_domain=ctx.backlog_domain,
|
|
25
|
+
api_key=ctx.api_key,
|
|
26
|
+
issue_key=issue_key,
|
|
27
|
+
description=description,
|
|
28
|
+
)
|
|
29
|
+
return result
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise e
|
app/utils/ultils.py
CHANGED
|
@@ -46,28 +46,29 @@ def time_in_range(time: str, start_range: str, end_range: str):
|
|
|
46
46
|
return start_range_time <= time_to_be_compared <= end_range_time
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def process_issue_detail(issue_detail, timezone, issue_key):
|
|
49
|
+
def process_issue_detail(issue_detail, timezone, issue_key, include_comments: bool = True):
|
|
50
50
|
processed_issue = {
|
|
51
51
|
"issue_key": issue_key,
|
|
52
52
|
"summary": issue_detail["summary"],
|
|
53
53
|
"description": issue_detail["description"]
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
if include_comments:
|
|
57
|
+
comments = issue_detail.get("comments", [])
|
|
58
|
+
if comments:
|
|
59
|
+
# Sort comments by created_at (created field)
|
|
60
|
+
sorted_comments = sorted(comments, key=lambda c: convert_to_timezone(timezone, c["created"]))
|
|
61
|
+
# Create list of {content, created_by} where created_by is just the name
|
|
62
|
+
processed_comments = [
|
|
63
|
+
{
|
|
64
|
+
"content": c["content"],
|
|
65
|
+
"created_by": c["createdUser"]["name"] if c.get("createdUser") else None
|
|
66
|
+
}
|
|
67
|
+
for c in sorted_comments if c.get("content")
|
|
68
|
+
]
|
|
69
|
+
# Filter out None created_by if any (though should not happen)
|
|
70
|
+
processed_comments = [c for c in processed_comments if c["created_by"]]
|
|
71
|
+
processed_issue["comments"] = processed_comments
|
|
71
72
|
|
|
72
73
|
return processed_issue
|
|
73
74
|
|
|
@@ -77,6 +78,7 @@ async def get_issue_detail_handler(
|
|
|
77
78
|
api_key: str,
|
|
78
79
|
issue_key: str,
|
|
79
80
|
timezone: str,
|
|
81
|
+
include_comments: bool = True,
|
|
80
82
|
):
|
|
81
83
|
issue_comments_url = f"{backlog_domain}api/v2/issues/{issue_key}/comments"
|
|
82
84
|
issue_detail_url = f"{backlog_domain}api/v2/issues/{issue_key}"
|
|
@@ -85,31 +87,38 @@ async def get_issue_detail_handler(
|
|
|
85
87
|
async with httpx.AsyncClient() as client:
|
|
86
88
|
try:
|
|
87
89
|
issue_detail_response = client.get(issue_detail_url, params=params)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
|
|
91
|
+
if include_comments:
|
|
92
|
+
comments_response = client.get(issue_comments_url, params=params)
|
|
93
|
+
results = await asyncio.gather(issue_detail_response, comments_response)
|
|
94
|
+
issue_detail_result = results[0]
|
|
95
|
+
comments_result = results[1]
|
|
96
|
+
issue_detail = issue_detail_result.json()
|
|
97
|
+
issue_comment = comments_result.json()
|
|
98
|
+
else:
|
|
99
|
+
issue_detail_result = await issue_detail_response
|
|
100
|
+
issue_detail = issue_detail_result.json()
|
|
101
|
+
issue_comment = []
|
|
94
102
|
|
|
95
|
-
if not
|
|
103
|
+
if not issue_detail_result.is_success:
|
|
96
104
|
error_code = issue_detail["errors"][0]["code"]
|
|
97
105
|
return {
|
|
98
106
|
"error_msg": BacklogApiError.get_description_by_code(error_code),
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
if not
|
|
109
|
+
if include_comments and not comments_result.is_success:
|
|
102
110
|
error_code = issue_comment["errors"][0]["code"]
|
|
103
111
|
return {
|
|
104
112
|
"error_msg": BacklogApiError.get_description_by_code(error_code),
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
if include_comments:
|
|
116
|
+
comments_in_time_range = []
|
|
117
|
+
for comment in issue_comment:
|
|
118
|
+
comments_in_time_range.append(comment)
|
|
119
|
+
issue_detail.update({"comments": comments_in_time_range})
|
|
110
120
|
|
|
111
|
-
issue_detail
|
|
112
|
-
processed_detail = process_issue_detail(issue_detail, timezone, issue_key)
|
|
121
|
+
processed_detail = process_issue_detail(issue_detail, timezone, issue_key, include_comments)
|
|
113
122
|
return processed_detail
|
|
114
123
|
|
|
115
124
|
except Exception as e:
|
|
@@ -291,3 +300,56 @@ async def get_current_user(backlog_domain: str, api_key: str) -> dict:
|
|
|
291
300
|
raise ValueError(f"Failed to get current user: {e}") from e
|
|
292
301
|
except Exception as e:
|
|
293
302
|
raise ValueError(f"Unexpected error while getting current user: {e}") from e
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
async def update_issue_description_handler(
|
|
306
|
+
backlog_domain: str,
|
|
307
|
+
api_key: str,
|
|
308
|
+
issue_key: str,
|
|
309
|
+
description: str,
|
|
310
|
+
):
|
|
311
|
+
"""Update issue description via Backlog API.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
backlog_domain (str): Backlog domain URL
|
|
315
|
+
api_key (str): API key for authentication
|
|
316
|
+
issue_key (str): Issue key or ID
|
|
317
|
+
description (str): New description content
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
dict: Updated issue information
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
url = f"{backlog_domain}api/v2/issues/{issue_key}"
|
|
324
|
+
params = {"apiKey": api_key}
|
|
325
|
+
data = {"description": description}
|
|
326
|
+
|
|
327
|
+
async with httpx.AsyncClient() as client:
|
|
328
|
+
response = await client.patch(
|
|
329
|
+
url,
|
|
330
|
+
params=params,
|
|
331
|
+
data=data,
|
|
332
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
333
|
+
timeout=10.0
|
|
334
|
+
)
|
|
335
|
+
response.raise_for_status()
|
|
336
|
+
result = response.json()
|
|
337
|
+
|
|
338
|
+
result = {
|
|
339
|
+
"issueKey": result["issueKey"],
|
|
340
|
+
"summary": result["summary"],
|
|
341
|
+
"description": result["description"]
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return result
|
|
345
|
+
except httpx.HTTPStatusError as e:
|
|
346
|
+
try:
|
|
347
|
+
body = e.response.json()
|
|
348
|
+
error_code = None
|
|
349
|
+
if "errors" in body and body["errors"]:
|
|
350
|
+
error_code = body["errors"][0].get("code")
|
|
351
|
+
raise ValueError(f"API error: {BacklogApiError.get_description_by_code(error_code)}") from e
|
|
352
|
+
except Exception:
|
|
353
|
+
raise ValueError("Failed to parse error response") from e
|
|
354
|
+
except Exception as e:
|
|
355
|
+
raise ValueError(f"Request failed: {str(e)}") from e
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
app/logging_config.py,sha256=1tSV1HXExfIi_mxOpugvGL7ywd43SMmsKsjARWXTLnU,999
|
|
3
|
-
app/main.py,sha256=
|
|
3
|
+
app/main.py,sha256=iCyuJoQlueVQjZWmAlmb1WuMBDO2arTHSjkjLoXCZqQ,527
|
|
4
4
|
app/server_settings.py,sha256=mI-G1DKJ2IBg9dKuW4Da5MDhFLeYSQwLOGGNEb_kQwM,434
|
|
5
5
|
app/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
app/constants/constants.py,sha256=DV8HJnvCceHqUmA5csk9CcmcX9Zr0upl56qXVa9tF88,1664
|
|
7
7
|
app/core/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
8
|
app/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
app/models/models.py,sha256=y3JhziNew3_2bM4Lw3aVjWcTvhmaEbzz0AaCHsdAkbk,4169
|
|
10
|
-
app/tools/__init__.py,sha256=
|
|
11
|
-
app/tools/get_issue_details.py,sha256=
|
|
10
|
+
app/tools/__init__.py,sha256=v37to4C68ZJswTDeECaZZT3IPbRIe876CzJ_0XmZYCw,108
|
|
11
|
+
app/tools/get_issue_details.py,sha256=efixu906oloeIZdf-geTlLxt9nLe-tPDqUlDd9HL6-g,973
|
|
12
12
|
app/tools/get_user_issue_list.py,sha256=soNX0OfvfT1hIyzeDMPaHpAjHeGqavtZdTl0CemCpyo,865
|
|
13
|
+
app/tools/update_issue_description.py,sha256=yS0yAETvDWeQYyeT-h7gTTihuszxyTJtizEyT6p4SX8,916
|
|
13
14
|
app/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
15
|
app/utils/di.py,sha256=5MmL6efLAX3uzp5WSMi4E_BDCgapzlr_OERZoN5NGnM,408
|
|
15
|
-
app/utils/ultils.py,sha256=
|
|
16
|
-
backlog_mcp-1.0.
|
|
17
|
-
backlog_mcp-1.0.
|
|
18
|
-
backlog_mcp-1.0.
|
|
19
|
-
backlog_mcp-1.0.
|
|
16
|
+
app/utils/ultils.py,sha256=_JGcpS-EMgp4gw_A3c4dTdPl5UEvVCM1x61n5VrQt70,13099
|
|
17
|
+
backlog_mcp-1.0.5.dist-info/METADATA,sha256=t774bjIDzAc1nbJBF6ahSq3j7rb4oSFFstufxSdzAQg,3883
|
|
18
|
+
backlog_mcp-1.0.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
+
backlog_mcp-1.0.5.dist-info/entry_points.txt,sha256=h5JeHYUp_uoaya4FO3QelcsgYOu-VRmh5qWRhPrwjtk,46
|
|
20
|
+
backlog_mcp-1.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|